blob: bb8fcec1dc83689ced0eddcab46bc41929b732bd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* 'License'); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
angular.module('dgc.lineage').controller('Lineage_ioController', ['$element', '$scope', '$state', '$stateParams', 'lodash', 'LineageResource', 'd3', 'DetailsResource', '$q',
function($element, $scope, $state, $stateParams, _, LineageResource, d3, DetailsResource, $q) {
var guidsList = [],
$$ = angular.element;
function inVertObj(edgs) {
var newEdgsObj = {};
$.each(edgs, function(key, value) {
for (var k = 0; k < value.length; k++) {
newEdgsObj[value[k]] = newEdgsObj[value[k]] || [];
newEdgsObj[value[k]] = [key];
}
});
return newEdgsObj;
}
function getCombinedLineageData(tableData, callRender) {
LineageResource.get({
tableName: tableData.tableName,
type: 'outputs'
}).$promise.then(
function lineageSuccess(response1) {
// $scope.$emit('show_lineage');
$('#lineageGraph').removeClass('hide');
LineageResource.get({
tableName: tableData.tableName,
type: 'inputs'
}).$promise.then(
//success
function lineageSuccess(response) {
if (response && response.results) {
response.results.values.edges = inVertObj(response.results.values.edges);
angular.forEach(response.results.values.edges, function(value, key) {
angular.forEach(response1.results.values.edges, function(value1, key1) {
if (key === key1) {
var array1 = value;
angular.forEach(value1, function(value2) {
array1.push(value2);
});
response.results.values.edges[key] = array1;
response1.results.values.edges[key] = array1;
}
});
});
angular.extend(response.results.values.edges, response1.results.values.edges);
angular.extend(response.results.values.vertices, response1.results.values.vertices);
if (!_.isEmpty(response.results.values.vertices)) {
loadProcess(response.results.values.edges, response.results.values.vertices)
.then(function(res) {
guidsList = res;
$scope.lineageData = transformData(response.results);
if (callRender) {
render();
}
});
} else {
$scope.requested = false;
}
} else {
$scope.requested = false;
}
},
function() {
$scope.requested = false;
}
);
},
function() {
$scope.requested = false;
}
);
}
function loadProcess(edges, vertices) {
var urlCalls = [];
var deferred = $q.defer();
for (var guid in edges) {
if (!vertices.hasOwnProperty(guid)) {
urlCalls.push(DetailsResource.get({
id: guid
}).$promise);
}
}
$q.all(urlCalls)
.then(function(results) {
deferred.resolve(results);
});
return deferred.promise;
}
$scope.type = $element.parent().attr('data-table-type');
$scope.requested = true;
$scope.height = $element[0].offsetHeight;
$scope.width = $element[0].offsetWidth;
function render() {
renderGraph($scope.lineageData, {
eleObj: $element,
element: $element[0],
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) {
if ($scope.requested) {
if ($scope.type === 'io') {
getCombinedLineageData(lineageData, true);
} else {
getCombinedLineageData(lineageData, true);
}
}
} else {
render();
}
}
$scope.guid = lineageData.guid;
});
function transformData(metaData) {
var edges = metaData.values.edges,
vertices = metaData.values.vertices,
nodes = {};
function getNode(guid) {
var name, type, tip;
if (vertices.hasOwnProperty(guid)) {
name = vertices[guid].values.name;
type = vertices[guid].values.vertexId.values.typeName;
} else {
var loadProcess = getLoadProcessTypes(guid);
if (typeof loadProcess !== "undefined") {
name = loadProcess.name;
type = 'edges';
tip = loadProcess.tip;
} else {
name = 'Load Process';
type = 'edges';
}
}
var vertex = {
guid: guid,
name: name,
type: type,
tip: tip
};
if (!nodes.hasOwnProperty(guid)) {
nodes[guid] = vertex;
}
return nodes[guid];
}
function getLoadProcessTypes(guid) {
var procesRes = [];
angular.forEach(guidsList, function(value) {
if (value.id.id === guid) {
procesRes.name = value.values.name;
procesRes.typeName = value.typeName;
procesRes.tip = value.values.queryText;
}
});
return procesRes;
}
function attachParent(edge, node) {
edge.forEach(function eachPoint(childGuid) {
var childNode = getNode(childGuid);
node.children = node.children || [];
node.children.push(childNode);
childNode.parent = node.guid;
});
}
/* Loop through all edges and attach them to correct parent */
for (var guid in edges) {
var edge = edges[guid],
node = getNode(guid);
/* Attach parent to each endpoint of edge */
attachParent(edge, node);
}
var starTingObj = {
name: 'root',
guid: 'root',
children: []
};
angular.forEach(nodes, function(value) {
if (!value.hasOwnProperty('parent')) {
starTingObj.children.push(value);
}
});
return starTingObj;
}
function renderGraph(data, container) {
// ************** Generate the tree diagram *****************
var element = d3.select(container.element),
widthg = Math.max(container.width, 1100),
heightg = Math.max((window.innerHeight - 400), 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().size([viewerHeight, viewerWidth]);
/*.size([viewerHeight, viewerWidth]); nodeSize([100, 200]);*/
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];
});
// 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) {
var toolTip = $$("<pre>").attr("class", "alert alert-success")
.append($$("<p>").html('Name :<b>' + d.name + '</b>'));
if (d.tip && d.tip.trim() !== "") {
toolTip.append($$("<p>").html('Query: ' + d.tip));
}
return toolTip.prop("outerHTML");
});
// 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)
.on("dblclick.zoom", function() {
return null;
})
.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 - 130;
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]);
}
//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
baseSvg.append("svg:defs")
.append("svg:marker")
.attr("id", "input-arrow")
.attr("viewBox", "0 0 10 10")
.attr("refX", -15)
.attr("refY", 5)
.attr("markerUnits", "strokeWidth")
.attr("markerWidth", 6)
.attr("markerHeight", 9)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M -2 5 L 8 0 L 8 10 z");
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) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
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();
nodes = _.uniq(nodes, 'guid');
_.each(nodes, function(o, i) {
var itemsOfTheSameDepth = _.where(nodes, {
depth: o.depth
});
var indexOfCurrent = _.indexOf(itemsOfTheSameDepth, o);
var interval = viewerHeight / itemsOfTheSameDepth.length;
nodes[i].x = interval / 2 + (interval * indexOfCurrent);
});
var links = tree.links(nodes);
_.each(links, function(o, i) {
//links[i].target = _.find(nodes, {guid: o.target.id});
links[i].target = _.find(nodes, {
guid: o.target.guid
});
});
// 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);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.call(dragListener)
.attr('class', function(d) {
if (d.guid === "root") {
return "hide";
} else {
return "";
}
})
.classed('node', true)
.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 && d.type !== '' && d.type.toLowerCase().indexOf('edges') !== -1) ? '../img/process.png' : '../img/tableicon.png';
})
.on('mouseover', function(d) {
if (d.type === 'edges' || 'Table') {
tooltip.show(d);
}
})
.on('dblclick', function(d) {
$state.go("details", {
id: d.guid
});
})
.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;
});
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// 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();
nodeExit.select("circle")
.attr("r", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links);
// .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', function(d) {
if (d.source.guid === "root") {
return "hide";
} else {
return "";
}
})
.classed('link', true)
.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(0,0)");
// Define the root
root = data;
root.x0 = viewerWidth / 2;
root.y0 = viewerHeight / 2;
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
$scope.requested = false;
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");
});
}
}
]);