You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@atlas.apache.org by ve...@apache.org on 2015/09/12 00:52:46 UTC
[09/11] incubator-atlas git commit: ATLAS-112 UI: Make lineage graph
extensible for multiple nodes. Contributed by Vishal Kadam
ATLAS-112 UI: Make lineage graph extensible for multiple nodes. Contributed by Vishal Kadam
Project: http://git-wip-us.apache.org/repos/asf/incubator-atlas/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-atlas/commit/1e1ed482
Tree: http://git-wip-us.apache.org/repos/asf/incubator-atlas/tree/1e1ed482
Diff: http://git-wip-us.apache.org/repos/asf/incubator-atlas/diff/1e1ed482
Branch: refs/heads/master
Commit: 1e1ed482cebff075664577d383d5e7a3bc6d12c3
Parents: ff5b1f1
Author: Venkatesh Seetharam <ve...@apache.org>
Authored: Fri Sep 11 15:16:37 2015 -0700
Committer: Venkatesh Seetharam <ve...@apache.org>
Committed: Fri Sep 11 15:16:37 2015 -0700
----------------------------------------------------------------------
dashboard/.jshintrc | 5 +-
.../public/modules/lineage/lineageController.js | 582 +++++++++++++++----
.../public/modules/lineage/views/lineage.html | 15 +-
release-log.txt | 2 +
4 files changed, 482 insertions(+), 122 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/1e1ed482/dashboard/.jshintrc
----------------------------------------------------------------------
diff --git a/dashboard/.jshintrc b/dashboard/.jshintrc
index 62b5e65..f42738e 100644
--- a/dashboard/.jshintrc
+++ b/dashboard/.jshintrc
@@ -39,7 +39,8 @@
"undef": true, // Require all non-global variables be declared before they are used.
"unused": true, // Warn unused variables.
"globals": { // Globals variables.
- "angular": true
+ "angular": true,
+ "$": false
},
"predef": [ // Extra globals.
"define",
@@ -60,4 +61,4 @@
"expect",
"ngGridFlexibleHeightPlugin"
]
-}
\ No newline at end of file
+}
http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/1e1ed482/dashboard/public/modules/lineage/lineageController.js
----------------------------------------------------------------------
diff --git a/dashboard/public/modules/lineage/lineageController.js b/dashboard/public/modules/lineage/lineageController.js
index b8bd09c..151002f 100644
--- a/dashboard/public/modules/lineage/lineageController.js
+++ b/dashboard/public/modules/lineage/lineageController.js
@@ -36,8 +36,9 @@ angular.module('dgc.lineage').controller('LineageController', ['$element', '$sco
render();
}
});
+ }else{
+ $scope.requested = false;
}
- $scope.requested = false;
});
}
@@ -62,16 +63,28 @@ angular.module('dgc.lineage').controller('LineageController', ['$element', '$sco
$scope.type = $element.parent().attr('data-table-type');
$scope.requested = false;
+ $scope.height = $element[0].offsetHeight;
+ $scope.width = $element[0].offsetWidth;
function render() {
renderGraph($scope.lineageData, {
+ eleObj : $element,
element: $element[0],
- height: $element[0].offsetHeight,
- width: $element[0].offsetWidth
+ height: $scope.height,
+ width: $scope.width
});
$scope.rendered = true;
}
+ $scope.onReset = function(){
+ renderGraph($scope.lineageData, {
+ eleObj : $element,
+ element: $element[0],
+ height: $scope.height,
+ width: $scope.width
+ });
+ };
+
$scope.$on('render-lineage', function(event, lineageData) {
if (lineageData.type === $scope.type) {
if (!$scope.lineageData) {
@@ -155,50 +168,249 @@ angular.module('dgc.lineage').controller('LineageController', ['$element', '$sco
}
function renderGraph(data, container) {
- // ************** Generate the tree diagram *****************
- var element = d3.select(container.element),
- width = Math.max(container.width, 960),
- height = Math.max(container.height, 350);
-
- var margin = {
- top: 100,
- right: 80,
- bottom: 30,
- left: 80
- };
- width = width - margin.right - margin.left;
- height = height - margin.top - margin.bottom;
-
- var i = 0;
-
- var tree = d3.layout.tree()
- .size([height, width]);
-
- var diagonal = d3.svg.diagonal()
- .projection(function(d) {
- return [d.y, d.x];
- });
+ // ************** Generate the tree diagram *****************
+ var element = d3.select(container.element),
+ widthg = Math.max(container.width, 960),
+ heightg = Math.max(container.height, 500),
+
+ totalNodes = 0,
+ maxLabelLength = 0,
+ selectedNode = null,
+ draggingNode = null,
+ dragListener = null,
+ dragStarted = true,
+ domNode = null,
+ multiParents = null,
+ nodes = null,
+ tooltip = null,
+ node = null,
+ i = 0,
+ duration = 750,
+ root,
+ depthwidth = 10;
+
+
+ var viewerWidth = widthg - 15,
+ viewerHeight = heightg;
+
+ var tree = d3.layout.tree().nodeSize([100, 200]);
+ /*.size([viewerHeight, viewerWidth]);*/
+
+ container.eleObj.find(".graph").html('');
+ container.eleObj.find("svg").remove();
+
+ // define a d3 diagonal projection for use by the node paths later on.
+ var diagonal = d3.svg.diagonal()
+ .projection(function(d) {
+ return [d.y, d.x];
+ });
- /* Initialize tooltip */
- var tooltip = d3.tip()
- .attr('class', 'd3-tip')
- .html(function(d) {
- return '<pre class="alert alert-success">' + d.tip + '</pre>';
- });
+ // A recursive helper function for performing some setup by walking through all nodes
+
+ function visit(parent, visitFn, childrenFn) {
+ if (!parent) return;
+
+ visitFn(parent);
+
+ var children = childrenFn(parent);
+ if (children) {
+ var count = children.length;
+ for (var i = 0; i < count; i++) {
+ visit(children[i], visitFn, childrenFn);
+ }
+ }
+ }
+
+ // Call visit function to establish maxLabelLength
+ visit(data, function(d) {
+ totalNodes++;
+ maxLabelLength = Math.max(d.name.length, maxLabelLength);
+
+ }, function(d) {
+ return d.children && d.children.length > 0 ? d.children : null;
+ });
+
+
+ // sort the tree according to the node names
+
+ function sortTree() {
+ tree.sort(function(a, b) {
+ return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1;
+ });
+ }
+ // Sort the tree initially incase the JSON isn't in a sorted order.
+ sortTree();
+
+ // Define the zoom function for the zoomable tree
+
+ function zoom() {
+ svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
+ }
+
+
+ // define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
+ var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
+ /* Initialize tooltip */
+ tooltip = d3.tip()
+ .attr('class', 'd3-tip')
+ .html(function(d) {
+ return '<pre class="alert alert-success">' + d.name + '</pre>';
+ });
- var svg = element.select('svg')
- .attr('width', width + margin.right + margin.left)
- .attr('height', height + margin.top + margin.bottom)
- /* Invoke the tip in the context of your visualization */
- .call(tooltip)
- .select('g')
- .attr('transform',
- 'translate(' + margin.left + ',' + margin.right + ')');
- //arrow
- svg.append("svg:defs").append("svg:marker").attr("id", "arrow").attr("viewBox", "0 0 10 10").attr("refX", 26).attr("refY", 5).attr("markerUnits", "strokeWidth").attr("markerWidth", 6).attr("markerHeight", 9).attr("orient", "auto").append("svg:path").attr("d", "M 0 0 L 10 5 L 0 10 z");
+ // define the baseSvg, attaching a class for styling and the zoomListener
+ var baseSvg = element.append('svg')
+ .attr("width", viewerWidth)
+ .attr("height", viewerHeight)
+ .attr("class", "overlay")
+ .call(zoomListener)
+ .call(tooltip);
+
+
+ // Define the drag listeners for drag/drop behaviour of nodes.
+ dragListener = d3.behavior.drag()
+ .on("dragstart", function(d) {
+ if (d ===root) {
+ return;
+ }
+ dragStarted = true;
+ nodes = tree.nodes(d);
+ d3.event.sourceEvent.stopPropagation();
+ // it's important that we suppress the mouseover event on the node being dragged. Otherwise it will absorb the mouseover event and the underlying node will not detect it d3.select(this).attr('pointer-events', 'none');
+ })
+ .on("dragend", function(d) {
+ if (d ===root) {
+ return;
+ }
+ domNode = this;
+ if (selectedNode) {
+ // now remove the element from the parent, and insert it into the new elements children
+ var index = draggingNode.parent.children.indexOf(draggingNode);
+ if (index > -1) {
+ draggingNode.parent.children.splice(index, 1);
+ }
+ if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') {
+ if (typeof selectedNode.children !== 'undefined') {
+ selectedNode.children.push(draggingNode);
+ } else {
+ selectedNode._children.push(draggingNode);
+ }
+ } else {
+ selectedNode.children = [];
+ selectedNode.children.push(draggingNode);
+ }
+ // Make sure that the node being added to is expanded so user can see added node is correctly moved
+ expand(selectedNode);
+ sortTree();
+ endDrag();
+ } else {
+ endDrag();
+ }
+ });
+
+ function endDrag() {
+ selectedNode = null;
+ d3.selectAll('.ghostCircle').attr('class', 'ghostCircle');
+ d3.select(domNode).attr('class', 'node');
+ // now restore the mouseover event or we won't be able to drag a 2nd time
+ d3.select(domNode).select('.ghostCircle').attr('pointer-events', '');
+ updateTempConnector();
+ if (draggingNode !== null) {
+ update(root);
+ centerNode(draggingNode);
+ draggingNode = null;
+ }
+ }
+
+
+ function expand(d) {
+ if (d._children) {
+ d.children = d._children;
+ d.children.forEach(expand);
+ d._children = null;
+ }
+ }
+
+ // Function to update the temporary connector indicating dragging affiliation
+ var updateTempConnector = function() {
+ var data = [];
+ if (draggingNode !== null && selectedNode !== null) {
+ // have to flip the source coordinates since we did this for the existing connectors on the original tree
+ data = [{
+ source: {
+ x: selectedNode.y0,
+ y: selectedNode.x0
+ },
+ target: {
+ x: draggingNode.y0,
+ y: draggingNode.x0
+ }
+ }];
+ }
+ var link = svgGroup.selectAll(".templink").data(data);
+
+ link.enter().append("path")
+ .attr("class", "templink")
+ .attr("d", d3.svg.diagonal())
+ .attr('pointer-events', 'none');
+
+ link.attr("d", d3.svg.diagonal());
+
+ link.exit().remove();
+ };
+
+ // Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
+
+ function centerNode(source) {
+ var scale = (depthwidth === 10) ? zoomListener.scale() : 0.4;
+ var x = -source.y0;
+ var y = -source.x0;
+ x = x * scale + 150;
+ y = y * scale + viewerHeight / 2;
+ d3.select('g').transition()
+ .duration(duration)
+ .attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
+ zoomListener.scale(scale);
+ zoomListener.translate([x, y]);
+ }
+
+ // Toggle children function
+
+ function toggleChildren(d) {
+ if (d.children) {
+ d._children = d.children;
+ d.children = null;
+ } else if (d._children) {
+ d.children = d._children;
+ d._children = null;
+ }
+ return d;
+ }
+
+ // Toggle children on click.
+
+ function click(d) {
+ if (d3.event.defaultPrevented) return; // click suppressed
+ d = toggleChildren(d);
+ update(d);
+ //centerNode(d);
+ }
+
+ //arrow
+ baseSvg.append("svg:defs")
+ .append("svg:marker")
+ .attr("id", "arrow")
+ .attr("viewBox", "0 0 10 10")
+ .attr("refX", 22)
+ .attr("refY", 5)
+ .attr("markerUnits", "strokeWidth")
+ .attr("markerWidth", 6)
+ .attr("markerHeight", 9)
+ .attr("orient", "auto")
+ .append("svg:path")
+ .attr("d", "M 0 0 L 10 5 L 0 10 z");
//marker for input type graph
- svg.append("svg:defs")
+ baseSvg.append("svg:defs")
.append("svg:marker")
.attr("id", "input-arrow")
.attr("viewBox", "0 0 10 10")
@@ -211,88 +423,228 @@ angular.module('dgc.lineage').controller('LineageController', ['$element', '$sco
.append("svg:path")
.attr("d", "M -2 5 L 8 0 L 8 10 z");
- var root = data;
-
- function update(source) {
+ function update(source) {
+ // Compute the new height, function counts total children of root node and sets tree height accordingly.
+ // This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
+ // This makes the layout more consistent.
+ var levelWidth = [1];
+ var childCount = function(level, n) {
- // Compute the new tree layout.
- var nodes = tree.nodes(source).reverse(),
- links = tree.links(nodes);
+ if (n.children && n.children.length > 0) {
+ if (levelWidth.length <= level + 1) levelWidth.push(0);
- // Normalize for fixed-depth.
- nodes.forEach(function(d) {
- d.y = d.depth * 180;
+ levelWidth[level + 1] += n.children.length;
+ n.children.forEach(function(d) {
+ childCount(level + 1, d);
});
+ }
+ };
+ childCount(0, root);
+ tree = tree.nodeSize([50, 100]);
+
+ // Compute the new tree layout.
+ var nodes = tree.nodes(root).reverse(),
+ links = tree.links(nodes);
+
+ // Set widths between levels based on maxLabelLength.
+ nodes.forEach(function(d) {
+ if(levelWidth.length > 1 && depthwidth === 10){
+ for(var o=0; o < levelWidth.length; o++){
+ if(levelWidth[o] > 4 ) { depthwidth = 70; break;}
+ }
+ }
+ var maxLebal = maxLabelLength;
+ if(depthwidth === 10) { maxLebal = 20;}
+ d.y = (d.depth * (maxLebal * depthwidth));
+ });
+
+ // Update the nodes…
+ node = svgGroup.selectAll("g.node")
+ .data(nodes, function(d) {
+ return d.id || (d.id = ++i);
+ });
- // Declare the nodes…
- var node = svg.selectAll('g.node')
- .data(nodes, function(d) {
- return d.id || (d.id = ++i);
- });
+ // Enter any new nodes at the parent's previous position.
+ var nodeEnter = node.enter().append("g")
+ .call(dragListener)
+ .attr("class", "node")
+ .attr("transform", function() {
+ return "translate(" + source.y0 + "," + source.x0 + ")";
+ })
+ .on('click', click);
+
+ nodeEnter.append("image")
+ .attr("class","nodeImage")
+ .attr("xlink:href", function(d) {
+ return d.type === 'Table' ? '../img/tableicon.png' : '../img/process.png';
+ })
+ .on('mouseover', function(d) {
+ if (d.type === 'LoadProcess' || 'Table') {
+ tooltip.show(d);
+ }
+ })
+ .on('mouseout', function(d) {
+ if (d.type === 'LoadProcess' || 'Table') {
+ tooltip.hide(d);
+ }
+ })
+ .attr("x", "-18px")
+ .attr("y", "-18px")
+ .attr("width", "34px")
+ .attr("height", "34px");
+
+ nodeEnter.append("text")
+ .attr("x", function(d) {
+ return d.children || d._children ? -10 : 10;
+ })
+ .attr("dx", function (d) { return d.children ? 50 : -50; })
+ .attr("dy", -24)
+ .attr('class', 'place-label')
+ .attr("text-anchor", function(d) {
+ return d.children || d._children ? "end" : "start";
+ })
+ .text(function(d) {
+ var nameDis = (d.name.length > 15) ? d.name.substring(0,15) + "..." : d.name;
+ $(this).attr('title', d.name);
+ return nameDis;
+ })
+ .style("fill-opacity", 0);
+
+ // Update the text to reflect whether node has children or not.
+ node.select('text')
+ .attr("x", function(d) {
+ return d.children || d._children ? -10 : 10;
+ })
+ .attr("text-anchor", function(d) {
+ return d.children || d._children ? "end" : "start";
+ })
+ .text(function(d) {
+ var nameDis = (d.name.length > 15) ? d.name.substring(0,15) + "..." : d.name;
+ $(this).attr('title', d.name);
+ return nameDis;
+ });
- // Enter the nodes.
- var nodeEnter = node.enter().append('g')
- .attr('class', 'node')
- .attr('transform', function(d) {
- return 'translate(' + d.y + ',' + d.x + ')';
- });
+ // Change the circle fill depending on whether it has children and is collapsed
+ // Change the circle fill depending on whether it has children and is collapsed
+ node.select("image.nodeImage")
+ .attr("r", 4.5)
+ .attr("xlink:href", function(d) {
+ if(d._children){
+ return d.type === 'Table' ? '../img/tableicon1.png' : '../img/process1.png';
+ }
+ return d.type === 'Table' ? '../img/tableicon.png' : '../img/process.png';
+ });
- nodeEnter.append("image")
- .attr("xlink:href", function(d) {
- //return d.icon;
- return d.type === 'Table' ? '../img/tableicon.png' : '../img/process.png';
- })
- .on('mouseover', function(d) {
- if (d.type === 'LoadProcess') {
- tooltip.show(d);
- }
- })
- .on('mouseout', function(d) {
- if (d.type === 'LoadProcess') {
- tooltip.hide(d);
- }
- })
- .attr("x", "-18px")
- .attr("y", "-18px")
- .attr("width", "34px")
- .attr("height", "34px");
-
- nodeEnter.append('text')
- .attr('x', function(d) {
- return d.children || d._children ?
- (5) * -1 : +15;
- })
- .attr('dy', '-1.75em')
- .attr('text-anchor', function(d) {
- return d.children || d._children ? 'middle' : 'middle';
- })
- .text(function(d) {
- return d.name;
- })
-
- .style('fill-opacity', 1);
-
- // Declare the links…
- var link = svg.selectAll('path.link')
- .data(links, function(d) {
- return d.target.id;
- });
- link.enter().insert('path', 'g')
- .attr('class', 'link')
- //.style('stroke', function(d) { return d.target.level; })
- .style('stroke', 'green')
- .attr('d', diagonal);
+ // Transition nodes to their new position.
+ var nodeUpdate = node.transition()
+ .duration(duration)
+ .attr("transform", function(d) {
+ return "translate(" + d.y + "," + d.x + ")";
+ });
- if ($scope.type === 'inputs') {
- link.attr("marker-start", "url(#input-arrow)"); //if input
- } else {
- link.attr("marker-end", "url(#arrow)"); //if input
- }
+ // Fade the text in
+ nodeUpdate.select("text")
+ .style("fill-opacity", 1);
- }
+ // Transition exiting nodes to the parent's new position.
+ var nodeExit = node.exit().transition()
+ .duration(duration)
+ .attr("transform", function() {
+ return "translate(" + source.y + "," + source.x + ")";
+ })
+ .remove();
- update(root);
+ nodeExit.select("circle")
+ .attr("r", 0);
+
+ nodeExit.select("text")
+ .style("fill-opacity", 0);
+
+ // Update the links…
+ var link = svgGroup.selectAll("path.link")
+ .data(links, function(d) {
+ return d.target.id;
+ });
+
+ // Enter any new links at the parent's previous position.
+ link.enter().insert("path", "g")
+ .attr("class", "link")
+ .style('stroke', 'green')
+ .attr("d", function() {
+ var o = {
+ x: source.x0,
+ y: source.y0
+ };
+ return diagonal({
+ source: o,
+ target: o
+ });
+ });
+
+ // Transition links to their new position.
+ link.transition()
+ .duration(duration)
+ .attr("d", diagonal);
+
+ // Transition exiting nodes to the parent's new position.
+ link.exit().transition()
+ .duration(duration)
+ .attr("d", function() {
+ var o = {
+ x: source.x,
+ y: source.y
+ };
+ return diagonal({
+ source: o,
+ target: o
+ });
+ })
+ .remove();
+
+ // Stash the old positions for transition.
+ nodes.forEach(function(d) {
+ d.x0 = d.x;
+ d.y0 = d.y;
+ });
+
+ if ($scope.type === 'inputs') {
+ link.attr("marker-start", "url(#input-arrow)"); //if input
+ } else {
+ link.attr("marker-end", "url(#arrow)"); //if input
+ }
+ }
+
+ // Append a group which holds all nodes and which the zoom Listener can act upon.
+ var svgGroup = baseSvg.append("g")
+ .attr("transform", "translate(120 ," + heightg/2 + ")");
+
+ // Define the root
+ root = data;
+ root.x0 = viewerHeight / 2;
+ root.y0 = 0;
+
+ // Layout the tree initially and center on the root node.
+ update(root);
+ centerNode(root);
+
+ var couplingParent1 = tree.nodes(root).filter(function(d) {
+ return d.name === 'cluster';
+ })[0];
+ var couplingChild1 = tree.nodes(root).filter(function(d) {
+ return d.name === 'JSONConverter';
+ })[0];
+
+ multiParents = [{
+ parent: couplingParent1,
+ child: couplingChild1
+ }];
+
+ multiParents.forEach(function() {
+ svgGroup.append("path", "g");
+ });
+
+
}
}
http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/1e1ed482/dashboard/public/modules/lineage/views/lineage.html
----------------------------------------------------------------------
diff --git a/dashboard/public/modules/lineage/views/lineage.html b/dashboard/public/modules/lineage/views/lineage.html
index e2e9ebd..3b412f7 100644
--- a/dashboard/public/modules/lineage/views/lineage.html
+++ b/dashboard/public/modules/lineage/views/lineage.html
@@ -17,9 +17,14 @@
-->
<div class="lineage-viz" data-ng-controller="LineageController">
- <h4 data-ng-if="!requested && !lineageData">No lineage data found</h4>
- <i data-ng-if="requested" class="fa fa-spinner fa-spin fa-5x"></i>
- <svg >
- <g/>
- </svg>
+ <button type="button" class="btn btn-primary pull-right" ng-click="onReset()">
+ Reset
+ </button>
+ <div class="graph">
+ <h4 data-ng-if="!requested && !lineageData">No lineage data found</h4>
+ <i data-ng-if="requested" class="fa fa-spinner fa-spin fa-5x"></i>
+ <svg >
+ <g/>
+ </svg>
+ </div>
</div>
http://git-wip-us.apache.org/repos/asf/incubator-atlas/blob/1e1ed482/release-log.txt
----------------------------------------------------------------------
diff --git a/release-log.txt b/release-log.txt
index dd50765..26e37d7 100644
--- a/release-log.txt
+++ b/release-log.txt
@@ -8,6 +8,8 @@ ATLAS-54 Rename configs in hive hook (shwethags)
ATLAS-3 Mixed Index creation fails with Date types (suma.shivaprasad via shwethags)
ALL CHANGES:
+ATLAS-112 UI: Make lineage graph extensible for multiple nodes (Vishal Kadam
+via Venkatesh Seetharam)
ATLAS-152 TimeStamp fields not showing the details tab (Vishal Kadam
via Venkatesh Seetharam)
ATLAS-111 UI: Create Help Link (Vishal Kadam via Venkatesh Seetharam)