|
AFRAME.registerComponent('graph', { |
|
schema: { |
|
csv: { |
|
type: 'string' |
|
}, |
|
id: { |
|
type: 'int', |
|
default: '0' |
|
}, |
|
width: { |
|
type: 'number', |
|
default: 1 |
|
}, |
|
height: { |
|
type: 'number', |
|
default: 1 |
|
}, |
|
depth: { |
|
type: 'number', |
|
default: 1 |
|
} |
|
}, |
|
|
|
/** |
|
* Called once when component is attached. Generally for initial setup. |
|
*/ |
|
update: function () { |
|
// Entity data |
|
var el = this.el; |
|
var object3D = el.object3D; |
|
var data = this.data; |
|
|
|
var width = data.width; |
|
var height = data.height; |
|
var depth = data.depth; |
|
|
|
// These will be used to set the range of the axes' scales |
|
var xRange = [0, width]; |
|
var yRange = [0, height]; |
|
var zRange = [0, -depth]; |
|
|
|
/** |
|
* Create origin point. |
|
* This gives a solid reference point for scaling data. |
|
* It is positioned at the vertex of the left grid and bottom grid (towards the front). |
|
*/ |
|
var originPointPosition = (-width / 2) + ' 0 ' + (depth / 2); |
|
var originPointID = 'originPoint' + data.id; |
|
|
|
d3.select(el).append('a-entity') |
|
.attr('id', originPointID) |
|
.attr('position', originPointPosition); |
|
|
|
// Create graphing area out of three textured planes |
|
var grid = gridMaker(width, height, depth); |
|
object3D.add(grid); |
|
|
|
// Label axes |
|
var xLabelPosition = (width / 3.2) + ' ' + '-0.1' + ' ' + '0'; |
|
d3.select('#' + originPointID) |
|
.append('a-entity') |
|
.attr('id', 'x') |
|
.attr('bmfont-text', 'text: Sepal Length (cm)') |
|
.attr('position', xLabelPosition); |
|
|
|
var yLabelPosition = (width + 0.05) + ' ' + (height / 2.2) + ' ' + (-depth); |
|
d3.select('#' + originPointID) |
|
.append('a-entity') |
|
.attr('id', 'y') |
|
.attr('bmfont-text', 'text: Petal Length (cm)') |
|
.attr('position', yLabelPosition); |
|
|
|
var zLabelPosition = (width + 0.02) + ' ' + '-0.05' + ' ' + (-depth / 2); |
|
d3.select('#' + originPointID) |
|
.append('a-entity') |
|
.attr('id', 'z') |
|
.attr('bmfont-text', 'text: Sepal Width (cm)') |
|
.attr('position', zLabelPosition); |
|
|
|
if (data.csv) { |
|
/* Plot data from CSV */ |
|
|
|
var originPoint = d3.select('#originPoint' + data.id); |
|
|
|
// Needed to assign species a color |
|
var cScale = d3.scale.ordinal() |
|
.domain(["Iris-virginica", "Iris-versicolor", "Iris-setosa"]) |
|
.range(["green", "blue", "red"]); |
|
|
|
// Convert CSV data from string to number |
|
d3.csv(data.csv, function (data) { |
|
data.forEach(function (d) { |
|
d.color = cScale(d.Species) |
|
}); |
|
plotData(data); |
|
}); |
|
|
|
var plotData = function (data) { |
|
// Scale x, y, and z values |
|
var xExtent = d3.extent(data, function (d) { return d.SepalLengthCm; }); |
|
var xScale = d3.scale.linear() |
|
.domain(xExtent) |
|
.range([xRange[0], xRange[1]]) |
|
.clamp('true'); |
|
|
|
var yExtent = d3.extent(data, function (d) { return d.PetalLengthCm; }); |
|
var yScale = d3.scale.linear() |
|
.domain(yExtent) |
|
.range([yRange[0], yRange[1]]); |
|
|
|
var zExtent = d3.extent(data, function (d) { return d.SepalWidthCm; }); |
|
var zScale = d3.scale.linear() |
|
.domain(zExtent) |
|
.range([zRange[0], zRange[1]]); |
|
|
|
// Append data to graph and attach event listeners |
|
originPoint.selectAll('a-sphere') |
|
.data(data) |
|
.enter() |
|
.append('a-sphere') |
|
.attr('radius', 0.03) |
|
.attr('color', function(d) { |
|
return d.color; |
|
}) |
|
.attr('position', function (d) { |
|
return xScale(d.SepalLengthCm) + ' ' + yScale(d.PetalLengthCm) + ' ' + zScale(d.SepalWidthCm); |
|
}) |
|
.on('mouseenter', mouseEnter); |
|
|
|
/** |
|
* Event listener adds and removes data labels. |
|
* "this" refers to sphere element of a given data point. |
|
*/ |
|
function mouseEnter () { |
|
// Get data |
|
var data = this.__data__; |
|
|
|
// Get width of graphBox (needed to set label position) |
|
var graphBoxEl = this.parentElement.parentElement; |
|
var graphBoxData = graphBoxEl.components.graph.data; |
|
var graphBoxWidth = graphBoxData.width; |
|
|
|
// Look for an existing label |
|
var oldLabel = d3.select('#tempDataLabel'); |
|
var oldLabelParent = oldLabel.select(function () { return this.parentNode; }); |
|
|
|
// Look for an existing beam |
|
var oldBeam = d3.select('#tempDataBeam'); |
|
|
|
// Look for an existing background |
|
var oldBackground = d3.select('#tempDataBackground'); |
|
|
|
// If there is no existing label, make one |
|
if (oldLabel[0][0] === null) { |
|
labelMaker(this, graphBoxWidth); |
|
} else { |
|
// Remove old label |
|
oldLabel.remove(); |
|
// Remove beam |
|
oldBeam.remove(); |
|
// Remove background |
|
oldBackground.remove(); |
|
// Create new label |
|
labelMaker(this, graphBoxWidth); |
|
} |
|
} |
|
}; |
|
} |
|
} |
|
}); |
|
|
|
/* HELPER FUNCTIONS */ |
|
|
|
/** |
|
* planeMaker() creates a plane given width and height (kind of). |
|
* It is used by gridMaker(). |
|
*/ |
|
function planeMaker (horizontal, vertical) { |
|
// Controls texture repeat for U and V |
|
var uHorizontal = horizontal * 4; |
|
var vVertical = vertical * 4; |
|
|
|
// Load a texture, set wrap mode to repeat |
|
var texture = new THREE.TextureLoader().load('https://cdn.rawgit.com/bryik/aframe-scatter-component/master/assets/grid.png'); |
|
texture.wrapS = THREE.RepeatWrapping; |
|
texture.wrapT = THREE.RepeatWrapping; |
|
texture.anisotropy = 16; |
|
texture.repeat.set(uHorizontal, vVertical); |
|
|
|
// Create material and geometry |
|
var material = new THREE.MeshBasicMaterial({map: texture, side: THREE.DoubleSide}); |
|
var geometry = new THREE.PlaneGeometry(horizontal, vertical); |
|
|
|
return new THREE.Mesh(geometry, material); |
|
} |
|
|
|
/** |
|
* gridMaker() creates a graphing box given width, height, and depth. |
|
* The textures are also scaled to these dimensions. |
|
* |
|
* There are many ways this function could be improved or done differently |
|
* e.g. buffer geometry, merge geometry, better reuse of material/geometry. |
|
*/ |
|
function gridMaker (width, height, depth) { |
|
var grid = new THREE.Object3D(); |
|
|
|
// AKA bottom grid |
|
var xGrid = planeMaker(width, depth); |
|
xGrid.rotation.x = 90 * (Math.PI / 180); |
|
grid.add(xGrid); |
|
|
|
// AKA far grid |
|
var yPlane = planeMaker(width, height); |
|
yPlane.position.y = (0.5) * height; |
|
yPlane.position.z = (-0.5) * depth; |
|
grid.add(yPlane); |
|
|
|
// AKA side grid |
|
var zPlane = planeMaker(depth, height); |
|
zPlane.position.x = (-0.5) * width; |
|
zPlane.position.y = (0.5) * height; |
|
zPlane.rotation.y = 90 * (Math.PI / 180); |
|
grid.add(zPlane); |
|
|
|
return grid; |
|
} |
|
|
|
/** |
|
* labelMaker() creates a label for a given data point and graph height. |
|
* dataEl - A data point's element. |
|
* graphBoxWidth - The width of the graph. |
|
*/ |
|
function labelMaker (dataEl, graphBoxWidth) { |
|
var dataElement = d3.select(dataEl); |
|
// Retrieve original data |
|
var dataValues = dataEl.__data__; |
|
|
|
// Create individual x, y, and z labels using original data values |
|
// round to 1 decimal space (should use d3 format for consistency later) |
|
var sepalLength = 'Sepal length (cm): ' + dataValues.SepalLengthCm + '\n \n'; |
|
var petalLength = 'Petal length (cm): ' + dataValues.PetalLengthCm + '\n \n'; |
|
var sepalWidth = 'Sepal width (cm): ' + dataValues.SepalWidthCm; |
|
var labelText = 'text: ' + sepalLength + petalLength + sepalWidth; |
|
|
|
// Position label right of graph |
|
var padding = 0.2; |
|
var sphereXPosition = dataEl.getAttribute('position').x; |
|
var labelXPosition = (graphBoxWidth + padding) - sphereXPosition; |
|
var labelPosition = labelXPosition + ' -0.43 0'; |
|
|
|
// Add pointer |
|
var beamWidth = labelXPosition; |
|
// The beam's pivot is in the center |
|
var beamPosition = (labelXPosition - (beamWidth / 2)) + '0 0'; |
|
dataElement.append('a-box') |
|
.attr('id', 'tempDataBeam') |
|
.attr('height', '0.01') |
|
.attr('width', beamWidth) |
|
.attr('depth', '0.01') |
|
.attr('color', 'purple') |
|
.attr('position', beamPosition); |
|
|
|
// Add label |
|
dataElement.append('a-entity') |
|
.attr('id', 'tempDataLabel') |
|
.attr('bmfont-text', labelText) |
|
.attr('position', labelPosition); |
|
|
|
var backgroundPosition = (labelXPosition + 1.15) + ' 0.02 -0.1'; |
|
// Add background card |
|
dataElement.append('a-plane') |
|
.attr('id', 'tempDataBackground') |
|
.attr('width', '2.3') |
|
.attr('height', '1.3') |
|
.attr('color', '#ECECEC') |
|
.attr('position', backgroundPosition); |
|
} |
@aronduby D3 has a lot of useful utilities (like scales) that made things a bit easier. A-Frame and D3 can work together (to a certain degree anyway).
Sorry it took so long to reply; apparently Gist comments don't trigger notifications...and I've just realized that you're unlikely to see this for the same reason 😑