| /** |
| * 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. |
| */ |
| |
| define(['require', |
| 'backbone', |
| 'hbs!tmpl/graph/RelationshipLayoutView_tmpl', |
| 'collection/VLineageList', |
| 'models/VEntity', |
| 'utils/Utils', |
| 'd3-tip', |
| 'utils/Enums', |
| 'utils/UrlLinks', |
| 'platform' |
| ], function(require, Backbone, RelationshipLayoutViewtmpl, VLineageList, VEntity, Utils, d3Tip, Enums, UrlLinks, platform) { |
| 'use strict'; |
| |
| var RelationshipLayoutView = Backbone.Marionette.LayoutView.extend( |
| /** @lends RelationshipLayoutView */ |
| { |
| _viewName: 'RelationshipLayoutView', |
| |
| template: RelationshipLayoutViewtmpl, |
| |
| /** Layout sub regions */ |
| regions: {}, |
| |
| /** ui selector cache */ |
| ui: { |
| relationshipDetailClose: '[data-id="close"]', |
| searchNode: '[data-id="searchNode"]' |
| }, |
| |
| /** ui events hash */ |
| events: function() { |
| var events = {}; |
| events["click " + this.ui.relationshipDetailClose] = function() { |
| this.toggleInformationSlider({ close: true }); |
| }; |
| events["keyup " + this.ui.searchNode] = 'searchNode'; |
| return events; |
| }, |
| |
| /** |
| * intialize a new RelationshipLayoutView Layout |
| * @constructs |
| */ |
| initialize: function(options) { |
| _.extend(this, _.pick(options, 'entity', 'entityName', 'guid', 'actionCallBack')); |
| this.graphData = this.createData(this.entity); |
| }, |
| createData: function(entity) { |
| var that = this, |
| links = [], |
| nodes = {}; |
| if (entity && entity.relationshipAttributes) { |
| _.each(entity.relationshipAttributes, function(obj, key) { |
| if (!_.isEmpty(obj)) { |
| links.push({ |
| "source": nodes[that.entity.typeName] || |
| (nodes[that.entity.typeName] = _.extend({ "name": that.entity.typeName }, { value: entity })), |
| "target": nodes[key] || |
| (nodes[key] = _.extend({ |
| "name": key |
| }, { value: obj })), |
| "value": obj |
| }) |
| } |
| }); |
| } |
| return { nodes: nodes, links: links }; |
| }, |
| onRender: function() { |
| |
| }, |
| onShow: function(argument) { |
| if (this.graphData && _.isEmpty(this.graphData.links)) { |
| this.noRelationship(); |
| } else { |
| this.createGraph(this.graphData); |
| } |
| }, |
| noRelationship: function() { |
| this.$('svg').html('<text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">No relationship data found</text>'); |
| }, |
| toggleInformationSlider: function(options) { |
| if (options.open && !this.$('.relationship-details').hasClass("open")) { |
| this.$('.relationship-details').addClass('open'); |
| } else if (options.close && this.$('.relationship-details').hasClass("open")) { |
| d3.selectAll('circle').attr("stroke", "none"); |
| this.$('.relationship-details').removeClass('open'); |
| } |
| }, |
| searchNode: function(e) { |
| var $el = $(e.currentTarget); |
| this.updateRelationshipDetails(_.extend({}, $el.data(), { searchString: $el.val() })) |
| }, |
| updateRelationshipDetails: function(options) { |
| var data = options.obj.value, |
| typeName = data.typeName || options.obj.name, |
| searchString = options.searchString, |
| listString = ""; |
| this.ui.searchNode.hide(); |
| this.$("[data-id='typeName']").text(typeName); |
| var getElement = function(options) { |
| var name = options.entityName ? options.entityName : Utils.getName(options, "displayText"); |
| return "<li><a href=#!/detailPage/" + options.guid + "?tabActive=relationship>" + _.escape(name) + " (" + options.typeName + ")</a></li>"; |
| } |
| if (_.isArray(data)) { |
| if (data.length > 1) { |
| this.ui.searchNode.show(); |
| } |
| _.each(_.sortBy(data, "displayText"), function(val) { |
| var name = Utils.getName(val, "displayText"), |
| valObj = _.extend({}, val, { entityName: name }); |
| if (searchString) { |
| if (name.search(new RegExp(searchString, "i")) != -1) { |
| listString += getElement(valObj); |
| } else { |
| return; |
| } |
| } else { |
| listString += getElement(valObj); |
| } |
| }); |
| } else { |
| listString += getElement(data); |
| } |
| this.$("[data-id='entityList']").html(listString); |
| }, |
| createGraph: function(data) { |
| var that = this, |
| width = this.$('svg').width(), |
| height = this.$('svg').height(); |
| |
| var scale = 1.0, |
| activeEntityColor = "#00b98b"; |
| |
| var force = d3.layout.force() |
| .nodes(d3.values(data.nodes)) |
| .links(data.links) |
| .size([width, height]) |
| .linkDistance(200) |
| .charge(function(d) { |
| var charge = -500; |
| if (d.index === 0) charge = 100 |
| return charge; |
| }) |
| .on("tick", tick) |
| .start(); |
| |
| var zoom = d3.behavior.zoom() |
| .scale(scale) |
| .scaleExtent([1, 5]) |
| .on("zoom", zoomed); |
| |
| function zoomed() { |
| container.attr("transform", |
| "translate(" + zoom.translate() + ")" + |
| "scale(" + zoom.scale() + ")" |
| ); |
| } |
| |
| function interpolateZoom(translate, scale) { |
| var self = this; |
| return d3.transition().duration(350).tween("zoom", function() { |
| var iTranslate = d3.interpolate(zoom.translate(), translate), |
| iScale = d3.interpolate(zoom.scale(), scale); |
| return function(t) { |
| zoom |
| .scale(iScale(t)) |
| .translate(iTranslate(t)); |
| zoomed(); |
| }; |
| }); |
| } |
| |
| function zoomClick() { |
| var clicked = d3.event.target, |
| direction = 1, |
| factor = 0.5, |
| target_zoom = 1, |
| center = [width / 2, height / 2], |
| extent = zoom.scaleExtent(), |
| translate = zoom.translate(), |
| translate0 = [], |
| l = [], |
| view = { x: translate[0], y: translate[1], k: zoom.scale() }; |
| |
| d3.event.preventDefault(); |
| direction = (this.id === 'zoom_in') ? 1 : -1; |
| target_zoom = zoom.scale() * (1 + factor * direction); |
| |
| if (target_zoom < extent[0] || target_zoom > extent[1]) { return false; } |
| |
| translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k]; |
| view.k = target_zoom; |
| l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y]; |
| |
| view.x += center[0] - l[0]; |
| view.y += center[1] - l[1]; |
| |
| interpolateZoom([view.x, view.y], view.k); |
| } |
| |
| |
| |
| d3.selectAll(this.$('span.lineageZoomButton')).on('click', zoomClick); |
| |
| var svg = d3.select(this.$("svg")[0]) |
| .attr("viewBox", "0 0 " + width + " " + height) |
| .attr("enable-background", "new 0 0 " + width + " " + height) |
| .call(zoom) |
| .on("dblclick.zoom", null), |
| drag = force.drag() |
| .on("dragstart", dragstart); |
| |
| var container = svg.append("g") |
| .attr("id", "container") |
| .attr("transform", "translate(0,0)scale(1,1)"); |
| |
| |
| // build the arrow. |
| container.append("svg:defs").selectAll("marker") |
| .data(["output"]) // Different link/path types can be defined here |
| .enter().append("svg:marker") // This section adds in the arrows |
| .attr("id", String) |
| .attr("viewBox", "0 -5 10 10") |
| .attr("refX", 10) |
| .attr("refY", -0.5) |
| .attr("markerWidth", 6) |
| .attr("markerHeight", 6) |
| .attr("orient", "auto") |
| .append("svg:path") |
| .attr("d", "M0,-5L10,0L0,5") |
| .attr("fill", activeEntityColor); |
| |
| // add the links and the arrows |
| var path = container.append("svg:g").selectAll("path") |
| .data(force.links()) |
| .enter().append("svg:path") |
| // .attr("class", function(d) { return "link " + d.type; }) |
| .attr("class", "relatioship-link") |
| .attr("stroke", activeEntityColor) |
| .attr("marker-end", "url(#output)"); |
| |
| // define the nodes |
| var node = container.selectAll(".node") |
| .data(force.nodes()) |
| .enter().append("g") |
| .attr("class", "node") |
| .on('touchstart', function(d) { |
| if (d && d.value && d.value.guid != that.guid) { |
| d3.event.stopPropagation(); |
| } |
| }) |
| .on('mousedown', function(d) { |
| if (d && d.value && d.value.guid != that.guid) { |
| d3.event.stopPropagation(); |
| } |
| }) |
| .on('click', function(d) { |
| if (d3.event.defaultPrevented) return; // ignore drag |
| that.toggleInformationSlider({ open: true, obj: d }); |
| that.ui.searchNode.data({ obj: d }); |
| d3.selectAll('circle').attr("stroke", "none"); |
| d3.select('circle[typename="' + d.name + '"]').attr("stroke", function(d) { |
| if (d && d.value && d.value.guid == that.guid) { |
| return "#316132"; |
| } else { |
| return activeEntityColor; |
| } |
| }); |
| that.updateRelationshipDetails({ obj: d }); |
| |
| }).call(force.drag); |
| |
| // add the nodes |
| var circleContainer = node.append("g"); |
| circleContainer.on("dblclick", function(d) { |
| if ((_.isArray(d.value) && d.value.length == 1) || d.value.guid) { |
| var guid = _.isArray(d.value) ? _.first(d.value).guid : d.value.guid; |
| Utils.setUrl({ |
| url: '#!/detailPage/' + guid, |
| mergeBrowserUrl: false, |
| urlParams: { tabActive: 'relationship' }, |
| trigger: true |
| }); |
| } |
| }) |
| circleContainer.append("circle") |
| .attr("cx", 0) |
| .attr("cy", 0) |
| .attr("r", function(d) { |
| d.radius = 25; |
| return d.radius; |
| }) |
| .attr("fill", function(d) { |
| if (d && d.value && d.value.guid == that.guid) { |
| return activeEntityColor; |
| } else { |
| return "#e0e0e0"; |
| } |
| }) |
| .attr("typename", function(d) { |
| return d.name; |
| }) |
| circleContainer.append("text") |
| .attr('x', 0) |
| .attr('y', 0) |
| .attr('dy', (25 - 17)) |
| .attr("text-anchor", "middle") |
| .style("font-family", "FontAwesome") |
| .style('font-size', function(d) { return '25px'; }) |
| .text(function(d) { |
| var iconObj = Enums.graphIcon[d.name]; |
| if (iconObj && iconObj.textContent) { |
| return iconObj.textContent; |
| } else { |
| if (d && _.isArray(d.value) && d.value.length > 1) { |
| return '\uf0c5'; |
| } else { |
| return '\uf016'; |
| } |
| } |
| }) |
| .attr("fill", function(d) { |
| if (d && d.value && d.value.guid == that.guid) { |
| return "#fff"; |
| } else { |
| return "#000"; |
| } |
| }); |
| var countBox = circleContainer.append('g') |
| countBox.append("circle") |
| .attr("cx", 18) |
| .attr("cy", -20) |
| .attr("r", function(d) { |
| if (_.isArray(d.value) && d.value.length > 1) { |
| return 10; |
| } |
| }); |
| |
| countBox.append("text") |
| .attr('dx', 18) |
| .attr('dy', -16) |
| .attr("text-anchor", "middle") |
| .attr("fill", "#e0e0e0") |
| .text(function(d) { |
| if (_.isArray(d.value) && d.value.length > 1) { |
| return d.value.length; |
| } |
| }); |
| |
| |
| // add the text |
| node.append("text") |
| .attr("x", -15) |
| .attr("y", "35") |
| .text(function(d) { return d.name; }); |
| |
| // add the curvy lines |
| function tick() { |
| path.attr("d", function(d) { |
| var diffX = d.target.x - d.source.x, |
| diffY = d.target.y - d.source.y, |
| |
| // Length of path from center of source node to center of target node |
| pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY)), |
| |
| // x and y distances from center to outside edge of target node |
| offsetX = (diffX * d.target.radius) / pathLength, |
| offsetY = (diffY * d.target.radius) / pathLength; |
| |
| return "M" + d.source.x + "," + d.source.y + "A" + pathLength + "," + pathLength + " 0 0,1 " + (d.target.x - offsetX) + "," + (d.target.y - offsetY) |
| }); |
| |
| node.attr("transform", function(d) { |
| // if (d && d.value && d.value.guid == that.guid) { |
| // Center fixed node |
| // var damper = 0.1; |
| // d.x = (width / 2) |
| // d.y = (height / 2) |
| // } |
| return "translate(" + d.x + "," + d.y + ")"; |
| }); |
| } |
| |
| function dragstart(d) { |
| d3.select(this).classed("fixed", d.fixed = true); |
| } |
| } |
| |
| }); |
| return RelationshipLayoutView; |
| }); |