///////////////////////////////////////////////////////////////////////////////////////// // Copyright (c) 2017 Koren Lev (Cisco Systems), Yaron Yogev (Cisco Systems) and others / // / // All rights reserved. This program and the accompanying materials / // are made available under the terms of the Apache License, Version 2.0 / // which accompanies this distribution, and is available at / // http://www.apache.org/licenses/LICENSE-2.0 / ///////////////////////////////////////////////////////////////////////////////////////// var chartOffset = 0; // D3.layout.force3d.js // (C) 2012 ziggy.jonsson.nyc@gmail.com // BSD license (http://opensource.org/licenses/BSD-3-Clause) d3.layout.force3d = function() { var forceXY = d3.layout.force() ,forceZ = d3.layout.force() ,zNodes = {} ,zLinks = {} ,nodeID = 1 ,linkID = 1 ,tickFunction = Object var force3d = {} Object.keys(forceXY).forEach(function(d) { force3d[d] = function() { var result = forceXY[d].apply(this,arguments) if (d !="nodes" && d!="links") forceZ[d].apply(this,arguments) return (result == forceXY) ? force3d : result } }) force3d.on = function(name,fn) { tickFunction = fn return force3d } forceXY.on("tick",function() { // Refresh zNodes add new, delete removed var _zNodes = {} forceXY.nodes().forEach(function(d,i) { if (!d.id) d.id = nodeID++ _zNodes[d.id] = zNodes[d.id] || {x:d.z,px:d.z,py:d.z,y:d.z,id:d.id} d.z = _zNodes[d.id].x }) zNodes = _zNodes // Refresh zLinks add new, delete removed var _zLinks = {} forceXY.links().forEach(function(d) { var nytt = false if (!d.linkID) { d.linkID = linkID++;nytt=true} _zLinks[d.linkID] = zLinks[d.linkID] || {target:zNodes[d.target.id],source:zNodes[d.source.id]} }) zLinks = _zLinks // Update the nodes/links in forceZ forceZ.nodes(d3.values(zNodes)) forceZ.links(d3.values(zLinks)) forceZ.start() // Need to kick forceZ so we don't lose the update mechanism // And run the user defined function, if defined tickFunction() }) // Expose the sub-forces for debugging purposes force3d.xy = forceXY force3d.z = forceZ return force3d } // end of d3.layout.force3d.js // Override default functions for d3 THREE.Object3D.prototype.appendChild = function (c) { this.add(c); return c; }; THREE.Object3D.prototype.querySelectorAll = function () { return []; }; // this one is to use D3's .attr() on THREE's objects THREE.Object3D.prototype.setAttribute = function (name, value) { var chain = name.split('.'); var object = this; for (var i = 0; i < chain.length - 1; i++) { object = object[chain[i]]; } object[chain[chain.length - 1]] = value; } // d3three object D3THREE = function(singleton) { this.labelGroup = new THREE.Object3D(); this.maxY = 0; this.axisObjects = {}; this.running = true; if (singleton) { if (typeof(d3three) !== 'undefined') { d3three.stop(); } d3three = this; } //if (!singleton) { // d3threes.push(this); //} } D3THREE.prototype.init = function(divId) { // standard THREE stuff, straight from examples this.renderer = new THREE.WebGLRenderer({antialias: true, alpha : true, preserveDrawingBuffer: true}); this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.type = THREE.PCFSoftShadow; this.renderer.shadowMapSoft = true; this.renderer.shadowCameraNear = 1000; this.renderer.shadowCameraFar = 10000; this.renderer.shadowCameraFov = 50; this.renderer.shadowMapBias = 0.0039; this.renderer.shadowMapDarkness = 0.25; this.renderer.shadowMapWidth = 10000; this.renderer.shadowMapHeight = 10000; this.renderer.physicallyBasedShading = true; this.divId = divId; this.width = document.getElementById(divId).offsetWidth; this.height = document.getElementById(divId).offsetHeight; this.renderer.setSize( this.width, this.height ); document.getElementById(divId).appendChild( this.renderer.domElement ); this.camera = new THREE.PerspectiveCamera( 30, this.width / this.height, 1, 100000 ); this.camera.position.z = -1000; this.camera.position.x = -800; this.camera.position.y = 600; this.controls = new THREE.OrbitControls( this.camera, this.renderer.domElement ); this.scene = new THREE.Scene(); this.defaultLight = new THREE.AmbientLight( 0xbbbbb ); // soft white light this.scene.add( this.defaultLight ); this.scene.add(this.labelGroup); var self = this; window.addEventListener( 'resize', self.onWindowResize.bind(self), false ); } D3THREE.prototype.onWindowResize = function() { var self = this; self.camera.aspect = self.width / self.height; self.camera.updateProjectionMatrix(); self.renderer.setSize( self.width, self.height ); } D3THREE.prototype.animate = function() { var self = this; if (this.running) { setTimeout( function() { this.requestId = requestAnimationFrame( self.animate.bind(self) ); }, 1000 / 15 ); self.renderer.render( self.scene, self.camera ); self.controls.update(); self.labelGroup.children.forEach(function(l){ l.rotation.setFromRotationMatrix(self.camera.matrix, "YXZ"); l.rotation.x = 0; l.rotation.z = 0; }); } else { window.removeEventListener( 'resize', self.onWindowResize.bind(self) ); while (self.scene.children.length > 0) { var childObject = self.scene.children[0]; if (childObject.geometry) { childObject.geometry.dispose(); } if (childObject.material) { childObject.material.dispose(); } self.scene.remove(childObject); delete(childObject); } self.renderer.context = null; self.renderer.domElement = null; self.renderer = null; self.camera = null; self.controls = null; self.scene = null; self.labelGroup = null; cancelAnimationFrame(self.requestId); } } D3THREE.prototype.stop = function() { this.running = false; } D3THREE.prototype.render = function(element, data) { element.render(data); } D3THREE.createAxis = function(dt) { return new D3THREE.Axis(dt); } // d3three axis D3THREE.Axis = function(dt) { this._scale = d3.scale.linear(); this._orient = "x"; this._tickFormat = function(d) { return d }; this._dt = dt; } D3THREE.Axis.prototype.orient = function(o) { if (o) { this._dt.axisObjects[o] = this; this._orient = o; } return this; } D3THREE.Axis.prototype.scale = function(s) { if (s) { this._scale = s; } return this; } D3THREE.Axis.prototype.tickFormat = function(f) { if (f) { this._tickFormat = f; } return this; } D3THREE.Axis.prototype.interval = function() { var interval; if (typeof(this._scale.rangeBand) === 'function') { // ordinal scale interval = this._scale.range()[1]; } else { interval = this._scale.range()[1] / (this._scale.ticks().length - 1); } return interval; } D3THREE.Axis.prototype.ticks = function() { var ticks; if (typeof(this._scale.rangeBand) === 'function') { // ordinal scale ticks = this._scale.domain(); } else { ticks = this._scale.ticks(); } return ticks; } D3THREE.Axis.prototype.getRotationShift = function() { return this.interval() * (this.ticks().length - 1) / 2; } D3THREE.Axis.prototype.render = function() { var material = new THREE.LineBasicMaterial({ color: 0xbbbbbb, linewidth: 2 }); var tickMaterial = new THREE.LineBasicMaterial({ color: 0xbbbbbb, linewidth: 1 }); var geometry = new THREE.Geometry(); interval = this.interval(); var interval = this.interval(), ticks = this.ticks(); // x,y axis shift, so rotation is from center of screen var xAxisShift = this._dt.axisObjects.x.getRotationShift(), yAxisShift = this._dt.axisObjects.y.getRotationShift(); for (var i = 0; i < ticks.length; i++) { var tickMarGeometry = new THREE.Geometry(); var shape = new THREE.TextGeometry(this._tickFormat(ticks[i]), { size: 5, height: 1, curveSegments: 20 }); var wrapper = new THREE.MeshBasicMaterial({color: 0xbbbbbb}); var words = new THREE.Mesh(shape, wrapper); if (this._orient === "y") { // tick geometry.vertices.push(new THREE.Vector3(i * interval - yAxisShift, chartOffset, 0 - xAxisShift)); tickMarGeometry.vertices.push(new THREE.Vector3(i * interval - yAxisShift, chartOffset, 0 - xAxisShift)); tickMarGeometry.vertices.push(new THREE.Vector3(i * interval - yAxisShift, -10 + chartOffset, 0 - xAxisShift)); var tickLine = new THREE.Line(tickMarGeometry, tickMaterial); this._dt.scene.add(tickLine); if (i * interval > this._dt.maxY) { this._dt.maxY = i * interval; } words.position.set(i * interval - yAxisShift, -20 + chartOffset, 0 - xAxisShift); } else if (this._orient === "z") { // tick geometry.vertices.push(new THREE.Vector3(0 + this._dt.maxY - yAxisShift, i * interval + chartOffset, 0 - xAxisShift)); tickMarGeometry.vertices.push(new THREE.Vector3(0 + this._dt.maxY - yAxisShift, i * interval + chartOffset, 0 - xAxisShift)); tickMarGeometry.vertices.push(new THREE.Vector3(10 + this._dt.maxY - yAxisShift, i * interval + chartOffset, 0 - xAxisShift)); var tickLine = new THREE.Line(tickMarGeometry, tickMaterial); this._dt.scene.add(tickLine); words.position.set(20 + this._dt.maxY - yAxisShift, i * interval + chartOffset, 0 - xAxisShift); } else if (this._orient === "x") { // tick geometry.vertices.push(new THREE.Vector3(0 - yAxisShift, chartOffset, i * interval - xAxisShift)); tickMarGeometry.vertices.push(new THREE.Vector3(0 - yAxisShift, 0 + chartOffset, i * interval - xAxisShift)); tickMarGeometry.vertices.push(new THREE.Vector3(0 - yAxisShift, -10 + chartOffset, i * interval - xAxisShift)); var tickLine = new THREE.Line(tickMarGeometry, tickMaterial); this._dt.scene.add(tickLine); words.position.set(0 - yAxisShift, -20 + chartOffset, i * interval - xAxisShift); } this._dt.labelGroup.add(words); } var line = new THREE.Line(geometry, material); this._dt.scene.add(line); } // Chart object D3THREE.Chart = function() { } D3THREE.Chart.prototype.config = function(c) { this._config = $.extend(this._config, c); } D3THREE.Chart.prototype.init = function(dt) { this._dt = dt; // mouse move var self = this; this._dt.renderer.domElement.addEventListener( 'mousemove', function(e) { self.onDocumentMouseMove(e); }, false ); } var cumulativeOffset = function(element) { var top = 0, left = 0; do { top += element.offsetTop || 0; left += element.offsetLeft || 0; element = element.offsetParent; } while(element); return { top: top, left: left }; }; D3THREE.Chart.prototype.detectNodeHover = function(e) { var boundingRect = this._dt.renderer.domElement.getBoundingClientRect(); var vector = new THREE.Vector3(); vector.x = ( (e.clientX - boundingRect.left) / this._dt.renderer.domElement.width ) * 2 - 1; vector.y = 1 - ( (e.clientY - boundingRect.top) / this._dt.renderer.domElement.height ) * 2; vector.z = 1; // create a check ray vector.unproject( this._dt.camera ); var ray = new THREE.Raycaster( this._dt.camera.position, vector.sub( this._dt.camera.position ).normalize() ); var intersects = ray.intersectObjects( this._nodeGroup.children ); for (var i = 0; i < this._nodeGroup.children.length; i++) { this._nodeGroup.children[i].material.opacity = 1; } if (intersects.length > 0) { var obj = intersects[0].object; obj.material.opacity = 0.5; var html = ""; html += "
"; html += ""; html += "x: " + this._dt.axisObjects.x._tickFormat(obj.userData.x); html += "
"; html += ""; html += "y: " + this._dt.axisObjects.y._tickFormat(obj.userData.y); html += "
"; html += ""; html += "z: " + this._dt.axisObjects.z._tickFormat(obj.userData.z); html += "
"; html += "
"; document.getElementById("tooltip-container").innerHTML = html; document.getElementById("tooltip-container").style.display = "block"; document.getElementById("tooltip-container").style.top = (e.pageY + 10) + "px"; document.getElementById("tooltip-container").style.left = (e.pageX + 10) + "px"; } else { document.getElementById("tooltip-container").style.display = "none"; } } // Scatter plot D3THREE.Scatter = function(dt) { this.init(dt); this._nodeGroup = new THREE.Object3D(); this._config = {color: 0x4682B4, pointRadius: 5}; } D3THREE.Scatter.prototype = new D3THREE.Chart(); D3THREE.Scatter.prototype.onDocumentMouseMove = function(e) { // detect intersected spheres this.detectNodeHover(e); } D3THREE.Scatter.prototype.render = function(data) { var geometry = new THREE.SphereGeometry( this._config.pointRadius, 32, 32 ); this._dt.scene.add(this._nodeGroup); // x,y axis shift, so rotation is from center of screen var xAxisShift = this._dt.axisObjects.x.getRotationShift(), yAxisShift = this._dt.axisObjects.y.getRotationShift(); var self = this; d3.select(this._nodeGroup) .selectAll() .data(data) .enter().append( function(d) { var material = new THREE.MeshBasicMaterial( { color: self._config.color } ); var mesh = new THREE.Mesh( geometry, material ); mesh.userData = {x: d.x, y: d.y, z: d.z}; return mesh; } ) .attr("position.z", function(d) { return self._dt.axisObjects.x._scale(d.x) - xAxisShift; }) .attr("position.x", function(d) { return self._dt.axisObjects.y._scale(d.y) - yAxisShift; }) .attr("position.y", function(d) { return self._dt.axisObjects.z._scale(d.z) + chartOffset; }); } // Surface plot D3THREE.Surface = function(dt) { this.init(dt); this._nodeGroup = new THREE.Object3D(); this._config = {color: 0x4682B4, pointColor: 0xff7f0e, pointRadius: 2}; } D3THREE.Surface.prototype = new D3THREE.Chart(); D3THREE.Surface.prototype.onDocumentMouseMove = function(e) { // detect intersected spheres var boundingRect = this._dt.renderer.domElement.getBoundingClientRect(); var vector = new THREE.Vector3(); vector.x = ( (e.clientX - boundingRect.left) / this._dt.renderer.domElement.width ) * 2 - 1; vector.y = 1 - ( (e.clientY - boundingRect.top) / this._dt.renderer.domElement.height ) * 2; vector.z = 1; // create a check ray vector.unproject( this._dt.camera ); var ray = new THREE.Raycaster( this._dt.camera.position, vector.sub( this._dt.camera.position ).normalize() ); var meshIntersects = ray.intersectObjects( [this._meshSurface] ); if (meshIntersects.length > 0) { for (var i = 0; i < this._nodeGroup.children.length; i++) { this._nodeGroup.children[i].visible = true; this._nodeGroup.children[i].material.opacity = 1; } this.detectNodeHover(e); } else { // hide nodes for (var i = 0; i < this._nodeGroup.children.length; i++) { this._nodeGroup.children[i].visible = false; } } } D3THREE.Surface.prototype.render = function(threeData) { /* render data points */ var geometry = new THREE.SphereGeometry( this._config.pointRadius, 32, 32 ); this._dt.scene.add(this._nodeGroup); // x,y axis shift, so rotation is from center of screen var xAxisShift = this._dt.axisObjects.x.getRotationShift(), yAxisShift = this._dt.axisObjects.y.getRotationShift(); var self = this; d3.select(this._nodeGroup) .selectAll() .data(threeData) .enter().append( function(d) { var material = new THREE.MeshBasicMaterial( { color: self._config.pointColor } ); var mesh = new THREE.Mesh( geometry, material ); mesh.userData = {x: d.x, y: d.y, z: d.z}; mesh.visible = false; return mesh; } ) .attr("position.z", function(d) { return self._dt.axisObjects.x._scale(d.x) - xAxisShift; }) .attr("position.x", function(d) { return self._dt.axisObjects.y._scale(d.y) - yAxisShift; }) .attr("position.y", function(d) { return self._dt.axisObjects.z._scale(d.z) + chartOffset; }); /* custom surface */ function distance (v1, v2) { var dx = v1.x - v2.x; var dy = v1.y - v2.y; var dz = v1.z - v2.z; return Math.sqrt(dx*dx+dz*dz); } var vertices = []; var holes = []; var triangles, mesh; var geometry = new THREE.Geometry(); var material = new THREE.MeshBasicMaterial({color: this._config.color}); for (var i = 0; i < threeData.length; i++) { vertices.push(new THREE.Vector3( self._dt.axisObjects.y._scale(threeData[i].y) - yAxisShift, self._dt.axisObjects.z._scale(threeData[i].z) + chartOffset, self._dt.axisObjects.x._scale(threeData[i].x) - xAxisShift)); } geometry.vertices = vertices; for (var i = 0; i < vertices.length; i++) { // find three closest vertices to generate surface var v1, v2, v3; var distances = []; // find vertices in same y or y + 1 row var minY = Number.MAX_VALUE; for (var j = i + 1; j < vertices.length; j++) { if (i !== j && vertices[j].x > vertices[i].x) { if (vertices[j].x < minY) { minY = vertices[j].x; } } } var rowVertices = [], row2Vertices = []; for (var j = i + 1; j < vertices.length; j++) { if (i !== j && (vertices[j].x === vertices[i].x)) { rowVertices.push({index: j, v: vertices[j]}); } if (i !== j && (vertices[j].x === minY)) { row2Vertices.push({index: j, v: vertices[j]}); } } if (rowVertices.length >= 1 && row2Vertices.length >= 2) { // find smallest x rowVertices.sort(function(a, b) { if (a.v.z < b.v.z) { return -1; } else if (a.v.z === b.v.z) { return 0; } else { return 1; } }); v1 = rowVertices[0].index; row2Vertices.sort(function(a, b) { if (a.v.z < b.v.z) { return -1; } else if (a.v.z === b.v.z) { return 0; } else { return 1; } }); v2 = row2Vertices[0].index; v3 = row2Vertices[1].index; var fv = [i, v1, v2, v3]; fv = fv.sort(function(a, b) { if (a < b) return -1; else if (a === b) return 0; else return 1; }); geometry.faces.push( new THREE.Face3(fv[1], fv[0], fv[3])); geometry.faces.push( new THREE.Face3(fv[0], fv[2], fv[3])); } } this._meshSurface = new THREE.Mesh( geometry, material ); this._dt.scene.add(this._meshSurface); } // Bar plot D3THREE.Bar = function(dt) { this.init(dt); this._nodeGroup = new THREE.Object3D(); this._config = {color: 0x4682B4, barSize: 5}; } D3THREE.Bar.prototype = new D3THREE.Chart(); D3THREE.Bar.prototype.onDocumentMouseMove = function(e) { this.detectNodeHover(e); } D3THREE.Bar.prototype.render = function(threeData) { /* render data points */ this._dt.scene.add(this._nodeGroup); // x,y axis shift, so rotation is from center of screen var xAxisShift = this._dt.axisObjects.x.getRotationShift(), yAxisShift = this._dt.axisObjects.y.getRotationShift(); var self = this; d3.select(this._nodeGroup) .selectAll() .data(threeData) .enter().append( function(d) { var height = self._dt.axisObjects.z._scale(d.z) + chartOffset; var geometry = new THREE.BoxGeometry( self._config.barSize, height, self._config.barSize ); var material = new THREE.MeshBasicMaterial( { color: self._config.color } ); var mesh = new THREE.Mesh( geometry, material ); mesh.userData = {x: d.x, y: d.y, z: d.z}; return mesh; } ) .attr("position.z", function(d) { return self._dt.axisObjects.x._scale(d.x) - xAxisShift; }) .attr("position.x", function(d) { return self._dt.axisObjects.y._scale(d.y) - yAxisShift; }) .attr("position.y", function(d) { var height = self._dt.axisObjects.z._scale(d.z) + chartOffset; return height / 2; }); } // Force layout plot D3THREE.Force = function(dt) { this.init(dt); this._nodeGroup = new THREE.Object3D(); this._config = {color: 0x4682B4, linkColor: 0xcccccc, linkWidth: 1}; } D3THREE.Force.prototype = new D3THREE.Chart(); D3THREE.Force.prototype.onDocumentMouseMove = function(e) { } D3THREE.Force.prototype.render = function(threeData) { var spheres = [], three_links = []; // Define the 3d force var force = d3.layout.force3d() .nodes(sort_data=[]) .links(links=[]) .size([50, 50]) .gravity(0.3) .charge(-400) var DISTANCE = 1; for (var i = 0; i < threeData.nodes.length; i++) { sort_data.push({x:threeData.nodes.x + DISTANCE,y:threeData.nodes.y + DISTANCE,z:0}) // set up the sphere vars var radius = 5, segments = 16, rings = 16; // create the sphere's material var sphereMaterial = new THREE.MeshLambertMaterial({ color: this._config.color }); var sphere = new THREE.Mesh( new THREE.SphereGeometry( radius, segments, rings), sphereMaterial); spheres.push(sphere); // add the sphere to the scene this._dt.scene.add(sphere); } for (var i = 0; i < threeData.links.length; i++) { links.push({target:sort_data[threeData.links[i].target],source:sort_data[threeData.links[i].source]}); var material = new THREE.LineBasicMaterial({ color: this._config.linkColor, linewidth: this._config.linkWidth}); var geometry = new THREE.Geometry(); geometry.vertices.push( new THREE.Vector3( 0, 0, 0 ) ); geometry.vertices.push( new THREE.Vector3( 0, 0, 0 ) ); var line = new THREE.Line( geometry, material ); line.userData = { source: threeData.links[i].source, target: threeData.links[i].target }; three_links.push(line); this._dt.scene.add(line); force.start(); } // set up the axes var x = d3.scale.linear().domain([0, 350]).range([0, 10]), y = d3.scale.linear().domain([0, 350]).range([0, 10]), z = d3.scale.linear().domain([0, 350]).range([0, 10]); var self = this; force.on("tick", function(e) { for (var i = 0; i < sort_data.length; i++) { spheres[i].position.set(x(sort_data[i].x) * 40 - 40, y(sort_data[i].y) * 40 - 40,z(sort_data[i].z) * 40 - 40); for (var j = 0; j < three_links.length; j++) { var line = three_links[j]; var vi = -1; if (line.userData.source === i) { vi = 0; } if (line.userData.target === i) { vi = 1; } if (vi >= 0) { line.geometry.vertices[vi].x = x(sort_data[i].x) * 40 - 40; line.geometry.vertices[vi].y = y(sort_data[i].y) * 40 - 40; line.geometry.vertices[vi].z = y(sort_data[i].z) * 40 - 40; line.geometry.verticesNeedUpdate = true; } } } }); }