4176 lines
163 KiB
JavaScript
4176 lines
163 KiB
JavaScript
/***********************************************************************************************************************
|
|
* D3 Force Implementation, Playground and Oracle APEX Plugin: https://github.com/ogobrecht/d3-force-apex-plugin
|
|
*
|
|
* @param pDomContainerId The DOM container, where the graph should be rendered
|
|
* @param pOptions The configuration object to configure the graph, see also the API reference
|
|
* @param pApexPluginId If used as APEX plugin, this ID is the plugin identifier for the AJAX calls
|
|
* @param pApexPageItemsToSubmit If used as APEX plugin, these page items are submitted before the AJAX call
|
|
* @returns {graph} The graph function is returned to allow method chaining
|
|
*/
|
|
function netGobrechtsD3Force(pDomContainerId, pOptions, pApexPluginId, pApexPageItemsToSubmit) { // jshint ignore:line
|
|
/* exported netGobrechtsD3Force */
|
|
/* globals apex, $v, navigator, d3, document, console, window, clearInterval, ActiveXObject, DOMParser */
|
|
|
|
"use strict";
|
|
|
|
// setup graph variable
|
|
var v = {
|
|
"conf": {},
|
|
"confDefaults": {},
|
|
"data": {},
|
|
"dom": {},
|
|
"events": {},
|
|
"lib": {},
|
|
"main": {},
|
|
"status": {},
|
|
"tools": {},
|
|
"version": "2.0.3"
|
|
};
|
|
|
|
|
|
/*******************************************************************************************************************
|
|
* MAIN: INIT
|
|
*/
|
|
v.main.init = function() {
|
|
|
|
// save parameter for later use
|
|
v.dom.containerId = pDomContainerId || "D3Force" + Math.floor(Math.random() * 1000000);
|
|
v.confUser = pOptions || {};
|
|
v.status.apexPluginId = pApexPluginId;
|
|
//v.status.apexPageItemsToSubmit = (pApexPageItemsToSubmit === "" ? false :
|
|
// pApexPageItemsToSubmit.replace(/\s/g,"").split(","));
|
|
|
|
// initialize the graph function
|
|
v.main.setupConfiguration();
|
|
v.main.setupDom();
|
|
v.main.setupFunctionReferences();
|
|
};
|
|
|
|
|
|
/*******************************************************************************************************************
|
|
* MAIN: SETUP CONFIGURATION
|
|
*/
|
|
v.main.setupConfiguration = function() {
|
|
/* jshint -W074, -W071 */
|
|
// configure debug mode for APEX, can be overwritten by users configuration object
|
|
// or later on with the API debug method
|
|
v.conf.debug = (v.status.apexPluginId && apex.jQuery("#pdebug").length === 1);
|
|
v.status.debugPrefix = "D3 Force in DOM container #" + v.dom.containerId + ": ";
|
|
|
|
// status variables
|
|
v.status.customize = false;
|
|
v.status.customizeCurrentMenu = "nodes";
|
|
v.status.customizeCurrentTabPosition = null;
|
|
v.status.forceTickCounter = 0;
|
|
v.status.forceStartTime = 0;
|
|
v.status.forceRunning = false;
|
|
v.status.graphStarted = false;
|
|
v.status.graphRendering = false;
|
|
v.status.graphReady = false;
|
|
v.status.graphOldPositions = null;
|
|
v.status.sampleData = false;
|
|
|
|
// default configuration
|
|
v.confDefaults.minNodeRadius = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "number",
|
|
"val": 6,
|
|
"options": [12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
|
|
};
|
|
v.confDefaults.maxNodeRadius = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "number",
|
|
"val": 18,
|
|
"options": [36, 34, 32, 30, 28, 26, 24, 22, 20, 18, 16, 14, 12]
|
|
};
|
|
v.confDefaults.colorScheme = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "text",
|
|
"val": "color20",
|
|
"options": ["color20", "color20b", "color20c", "color10", "direct"]
|
|
};
|
|
v.confDefaults.dragMode = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "bool",
|
|
"val": true,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.pinMode = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "bool",
|
|
"val": false,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.nodeEventToStopPinMode = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "text",
|
|
"val": "contextmenu",
|
|
"options": ["none", "dblclick", "contextmenu"]
|
|
};
|
|
v.confDefaults.onNodeContextmenuPreventDefault = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "bool",
|
|
"val": false,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.nodeEventToOpenLink = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "text",
|
|
"val": "dblclick",
|
|
"options": ["none", "click", "dblclick", "contextmenu"]
|
|
};
|
|
v.confDefaults.nodeLinkTarget = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "text",
|
|
"val": "_blank",
|
|
"options": ["none", "_blank", "nodeID", "domContainerID"]
|
|
};
|
|
v.confDefaults.showLabels = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "bool",
|
|
"val": true,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.labelsCircular = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "bool",
|
|
"val": false,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.labelDistance = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "number",
|
|
"val": 12,
|
|
"options": [30, 28, 26, 24, 22, 20, 18, 16, 14, 12, 10, 8, 6, 4, 2]
|
|
};
|
|
v.confDefaults.preventLabelOverlappingOnForceEnd = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "bool",
|
|
"val": false,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.labelPlacementIterations = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "number",
|
|
"val": 250,
|
|
"options": [2000, 1000, 500, 250, 125]
|
|
};
|
|
v.confDefaults.showTooltips = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "bool",
|
|
"val": true,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.tooltipPosition = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "text",
|
|
"val": "svgTopRight",
|
|
"options": ["node", "svgTopLeft", "svgTopRight"]
|
|
};
|
|
v.confDefaults.alignFixedNodesToGrid = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "bool",
|
|
"val": false,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.gridSize = {
|
|
"display": true,
|
|
"relation": "node",
|
|
"type": "number",
|
|
"val": 50,
|
|
"options": [150, 140, 130, 120, 110, 100, 90, 80, 70, 60, 50, 40, 30, 20, 10]
|
|
};
|
|
|
|
v.confDefaults.linkDistance = {
|
|
"display": true,
|
|
"relation": "link",
|
|
"type": "number",
|
|
"val": 80,
|
|
"options": [120, 110, 100, 90, 80, 70, 60, 50, 40, 30, 20]
|
|
};
|
|
v.confDefaults.showLinkDirection = {
|
|
"display": true,
|
|
"relation": "link",
|
|
"type": "bool",
|
|
"val": true,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.showSelfLinks = {
|
|
"display": true,
|
|
"relation": "link",
|
|
"type": "bool",
|
|
"val": true,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.selfLinkDistance = {
|
|
"display": true,
|
|
"relation": "link",
|
|
"type": "number",
|
|
"val": 20,
|
|
"options": [30, 28, 26, 24, 22, 20, 18, 16, 14, 12, 10, 8]
|
|
};
|
|
|
|
v.confDefaults.useDomParentWidth = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "bool",
|
|
"val": false,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.width = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "number",
|
|
"val": 1200,
|
|
"options": [1200, 1150, 1100, 1050, 1000, 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350,
|
|
300
|
|
]
|
|
};
|
|
v.confDefaults.height = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "number",
|
|
"val": 650,
|
|
"options": [1200, 1150, 1100, 1050, 1000, 950, 900, 850, 800, 750, 700, 650, 600, 550, 500, 450, 400, 350,
|
|
300
|
|
]
|
|
};
|
|
v.confDefaults.setDomParentPaddingToZero = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "bool",
|
|
"val": false,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.showBorder = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "bool",
|
|
"val": true,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.showLegend = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "bool",
|
|
"val": true,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.showLoadingIndicatorOnAjaxCall = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "bool",
|
|
"val": true,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.lassoMode = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "bool",
|
|
"val": false,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.zoomMode = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "bool",
|
|
"val": false,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.minZoomFactor = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "number",
|
|
"val": 0.2,
|
|
"options": [1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1]
|
|
};
|
|
v.confDefaults.maxZoomFactor = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "number",
|
|
"val": 5,
|
|
"options": [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
|
|
};
|
|
v.confDefaults.transform = {
|
|
"display": false,
|
|
"relation": "graph",
|
|
"type": "object",
|
|
"val": {
|
|
"translate": [0, 0],
|
|
"scale": 1
|
|
}
|
|
};
|
|
v.confDefaults.autoRefresh = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "bool",
|
|
"val": false,
|
|
"options": [true, false]
|
|
};
|
|
v.confDefaults.refreshInterval = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "number",
|
|
"val": 5000,
|
|
"options": [60000, 30000, 15000, 10000, 5000, 2500]
|
|
};
|
|
v.confDefaults.chargeDistance = {
|
|
"display": false,
|
|
"relation": "graph",
|
|
"type": "number",
|
|
"val": Infinity,
|
|
"options": [Infinity, 25600, 12800, 6400, 3200, 1600, 800, 400, 200, 100],
|
|
"internal": true
|
|
};
|
|
v.confDefaults.charge = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "number",
|
|
"val": -350,
|
|
"options": [-1000, -950, -900, -850, -800, -750, -700, -650, -600, -550, -500, -450, -400, -350, -300, -250,
|
|
-200, -150, -100, -50, 0],
|
|
"internal": true
|
|
};
|
|
v.confDefaults.gravity = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "number",
|
|
"val": 0.1,
|
|
"options": [1.00, 0.95, 0.90, 0.85, 0.80, 0.75, 0.70, 0.65, 0.60, 0.55, 0.50, 0.45, 0.40, 0.35, 0.30, 0.25,
|
|
0.20, 0.15, 0.1, 0.05, 0.00
|
|
],
|
|
"internal": true
|
|
};
|
|
v.confDefaults.linkStrength = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "number",
|
|
"val": 1,
|
|
"options": [1.00, 0.95, 0.90, 0.85, 0.80, 0.75, 0.70, 0.65, 0.60, 0.55, 0.50, 0.45, 0.40, 0.35, 0.30, 0.25,
|
|
0.20, 0.15, 0.10, 0.05, 0.00
|
|
],
|
|
"internal": true
|
|
};
|
|
v.confDefaults.friction = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "number",
|
|
"val": 0.9,
|
|
"options": [1.00, 0.95, 0.90, 0.85, 0.80, 0.75, 0.70, 0.65, 0.60, 0.55, 0.50, 0.45, 0.40, 0.35, 0.30, 0.25,
|
|
0.20, 0.15, 0.10, 0.05, 0.00
|
|
],
|
|
"internal": true
|
|
};
|
|
v.confDefaults.theta = {
|
|
"display": true,
|
|
"relation": "graph",
|
|
"type": "number",
|
|
"val": 0.8,
|
|
"options": [1, 0.95, 0.9, 0.85, 0.8, 0.75, 0.7, 0.65, 0.6, 0.55, 0.5, 0.45, 0.4, 0.35, 0.3, 0.25, 0.2, 0.15,
|
|
0.1, 0.05, 0
|
|
],
|
|
"internal": true
|
|
};
|
|
|
|
// create intial configuration
|
|
v.conf.debug = (typeof v.confUser.debug !== "undefined" ? v.tools.parseBool(v.confUser.debug) : false);
|
|
|
|
v.conf.minNodeRadius = v.confUser.minNodeRadius || v.confDefaults.minNodeRadius.val;
|
|
v.conf.maxNodeRadius = v.confUser.maxNodeRadius || v.confDefaults.maxNodeRadius.val;
|
|
v.conf.colorScheme = v.confUser.colorScheme || v.confDefaults.colorScheme.val;
|
|
v.conf.dragMode = (typeof v.confUser.dragMode !== "undefined" ? v.tools.parseBool(v.confUser.dragMode) :
|
|
v.confDefaults.dragMode.val);
|
|
v.conf.pinMode = (typeof v.confUser.pinMode !== "undefined" ? v.tools.parseBool(v.confUser.pinMode) :
|
|
v.confDefaults.pinMode.val);
|
|
v.conf.nodeEventToStopPinMode = v.confUser.nodeEventToStopPinMode || v.confDefaults.nodeEventToStopPinMode.val;
|
|
v.conf.onNodeContextmenuPreventDefault = (typeof v.confUser.onNodeContextmenuPreventDefault !== "undefined" ?
|
|
v.tools.parseBool(v.confUser.onNodeContextmenuPreventDefault) :
|
|
v.confDefaults.onNodeContextmenuPreventDefault.val);
|
|
v.conf.nodeEventToOpenLink = v.confUser.nodeEventToOpenLink || v.confDefaults.nodeEventToOpenLink.val;
|
|
v.conf.nodeLinkTarget = v.confUser.nodeLinkTarget || v.confDefaults.nodeLinkTarget.val;
|
|
v.conf.showLabels = (typeof v.confUser.showLabels !== "undefined" ? v.tools.parseBool(v.confUser.showLabels) :
|
|
v.confDefaults.showLabels.val);
|
|
v.conf.labelsCircular = (typeof v.confUser.labelsCircular !== "undefined" ?
|
|
v.tools.parseBool(v.confUser.labelsCircular) : v.confDefaults.labelsCircular.val);
|
|
v.conf.labelDistance = v.confUser.labelDistance || v.confDefaults.labelDistance.val;
|
|
v.conf.preventLabelOverlappingOnForceEnd =
|
|
(typeof v.confUser.preventLabelOverlappingOnForceEnd !== "undefined" ?
|
|
v.tools.parseBool(v.confUser.preventLabelOverlappingOnForceEnd) :
|
|
v.confDefaults.preventLabelOverlappingOnForceEnd.val);
|
|
v.conf.labelPlacementIterations = v.confUser.labelPlacementIterations ||
|
|
v.confDefaults.labelPlacementIterations.val;
|
|
v.conf.showTooltips = (typeof v.confUser.showTooltips !== "undefined" ?
|
|
v.tools.parseBool(v.confUser.showTooltips) : v.confDefaults.showTooltips.val);
|
|
v.conf.tooltipPosition = v.confUser.tooltipPosition || v.confDefaults.tooltipPosition.val;
|
|
v.conf.alignFixedNodesToGrid = (typeof v.confUser.alignFixedNodesToGrid !== "undefined" ?
|
|
v.tools.parseBool(v.confUser.alignFixedNodesToGrid) : v.confDefaults.alignFixedNodesToGrid.val);
|
|
v.conf.gridSize = (v.confUser.gridSize && v.confUser.gridSize > 0 ?
|
|
v.confUser.gridSize : v.confDefaults.gridSize.val);
|
|
|
|
v.conf.linkDistance = v.confUser.linkDistance || v.confDefaults.linkDistance.val;
|
|
v.conf.showLinkDirection = (typeof v.confUser.showLinkDirection !== "undefined" ?
|
|
v.tools.parseBool(v.confUser.showLinkDirection) : v.confDefaults.showLinkDirection.val);
|
|
v.conf.showSelfLinks = (typeof v.confUser.showSelfLinks !== "undefined" ?
|
|
v.tools.parseBool(v.confUser.showSelfLinks) : v.confDefaults.showSelfLinks.val);
|
|
v.conf.selfLinkDistance = v.confUser.selfLinkDistance || v.confDefaults.selfLinkDistance.val;
|
|
|
|
v.conf.useDomParentWidth = (typeof v.confUser.useDomParentWidth !== "undefined" ?
|
|
v.tools.parseBool(v.confUser.useDomParentWidth) : v.confDefaults.useDomParentWidth.val);
|
|
v.conf.width = v.confUser.width || v.confDefaults.width.val;
|
|
v.conf.height = v.confUser.height || v.confDefaults.height.val;
|
|
v.conf.setDomParentPaddingToZero = (typeof v.confUser.setDomParentPaddingToZero !== "undefined" ?
|
|
v.tools.parseBool(v.confUser.setDomParentPaddingToZero) : v.confDefaults.setDomParentPaddingToZero.val);
|
|
v.conf.showBorder = (typeof v.confUser.showBorder !== "undefined" ? v.tools.parseBool(v.confUser.showBorder) :
|
|
v.confDefaults.showBorder.val);
|
|
v.conf.showLegend = (typeof v.confUser.showLegend !== "undefined" ? v.tools.parseBool(v.confUser.showLegend) :
|
|
v.confDefaults.showLegend.val);
|
|
v.conf.showLoadingIndicatorOnAjaxCall = (typeof v.confUser.showLoadingIndicatorOnAjaxCall !== "undefined" ?
|
|
v.tools.parseBool(v.confUser.showLoadingIndicatorOnAjaxCall) :
|
|
v.confDefaults.showLoadingIndicatorOnAjaxCall.val);
|
|
v.conf.lassoMode = (typeof v.confUser.lassoMode !== "undefined" ? v.tools.parseBool(v.confUser.lassoMode) :
|
|
v.confDefaults.lassoMode.val);
|
|
v.conf.zoomMode = (typeof v.confUser.zoomMode !== "undefined" ? v.tools.parseBool(v.confUser.zoomMode) :
|
|
v.confDefaults.zoomMode.val);
|
|
v.conf.minZoomFactor = v.confUser.minZoomFactor || v.confDefaults.minZoomFactor.val;
|
|
v.conf.maxZoomFactor = v.confUser.maxZoomFactor || v.confDefaults.maxZoomFactor.val;
|
|
v.conf.transform = v.confUser.transform || v.confDefaults.transform.val;
|
|
v.conf.autoRefresh = (typeof v.confUser.autoRefresh !== "undefined" ?
|
|
v.tools.parseBool(v.confUser.autoRefresh) : v.confDefaults.autoRefresh.val);
|
|
v.conf.refreshInterval = v.confUser.refreshInterval || v.confDefaults.refreshInterval.val;
|
|
v.conf.chargeDistance = v.confUser.chargeDistance || Infinity;
|
|
v.conf.charge = v.confUser.charge || v.confDefaults.charge.val;
|
|
v.conf.gravity = v.confUser.gravity || v.confDefaults.gravity.val;
|
|
v.conf.linkStrength = v.confUser.linkStrength || v.confDefaults.linkStrength.val;
|
|
v.conf.friction = v.confUser.friction || v.confDefaults.friction.val;
|
|
v.conf.theta = v.confUser.theta || v.confDefaults.theta.val;
|
|
|
|
v.conf.onNodeMouseenterFunction = v.confUser.onNodeMouseenterFunction || null;
|
|
v.conf.onNodeMouseleaveFunction = v.confUser.onNodeMouseleaveFunction || null;
|
|
v.conf.onNodeClickFunction = v.confUser.onNodeClickFunction || null;
|
|
v.conf.onNodeDblclickFunction = v.confUser.onNodeDblclickFunction || null;
|
|
v.conf.onNodeContextmenuFunction = v.confUser.onNodeContextmenuFunction || null;
|
|
v.conf.onLinkClickFunction = v.confUser.onLinkClickFunction || null;
|
|
v.conf.onLassoStartFunction = v.confUser.onLassoStartFunction || null;
|
|
v.conf.onLassoEndFunction = v.confUser.onLassoEndFunction || null;
|
|
|
|
// initialize sample data
|
|
/* jshint -W110 */
|
|
v.data.sampleData = '<data>' +
|
|
'<nodes ID="7839" LABEL="KING is THE KING, you know?" LABELCIRCULAR="true" COLORVALUE="10" ' +
|
|
'COLORLABEL="Accounting" SIZEVALUE="5000" LINK="http://apex.oracle.com/" ' +
|
|
'INFOSTRING="This visualization is based on the well known emp table." />' +
|
|
'<nodes ID="7698" LABEL="BLAKE" COLORVALUE="30" COLORLABEL="Sales" SIZEVALUE="2850" />' +
|
|
'<nodes ID="7782" LABEL="CLARK" COLORVALUE="10" COLORLABEL="Accounting" SIZEVALUE="2450" />' +
|
|
'<nodes ID="7566" LABEL="JONES" COLORVALUE="20" COLORLABEL="Research" SIZEVALUE="2975" />' +
|
|
'<nodes ID="7788" LABEL="SCOTT" COLORVALUE="20" COLORLABEL="Research" SIZEVALUE="3000" />' +
|
|
'<nodes ID="7902" LABEL="FORD" COLORVALUE="20" COLORLABEL="Research" SIZEVALUE="3000" />' +
|
|
'<nodes ID="7369" LABEL="SMITH" COLORVALUE="20" COLORLABEL="Research" SIZEVALUE="800" />' +
|
|
'<nodes ID="7499" LABEL="ALLEN" COLORVALUE="30" COLORLABEL="Sales" SIZEVALUE="1600" />' +
|
|
'<nodes ID="7521" LABEL="WARD" COLORVALUE="30" COLORLABEL="Sales" SIZEVALUE="1250" />' +
|
|
'<nodes ID="7654" LABEL="MARTIN" COLORVALUE="30" COLORLABEL="Sales" SIZEVALUE="1250" />' +
|
|
'<nodes ID="7844" LABEL="TURNER" COLORVALUE="30" COLORLABEL="Sales" SIZEVALUE="1500" />' +
|
|
'<nodes ID="7876" LABEL="ADAMS" COLORVALUE="20" COLORLABEL="Research" SIZEVALUE="1100" />' +
|
|
'<nodes ID="7900" LABEL="JAMES" COLORVALUE="30" COLORLABEL="Sales" SIZEVALUE="950" />' +
|
|
'<nodes ID="7934" LABEL="MILLER" COLORVALUE="10" COLORLABEL="Accounting" SIZEVALUE="1300" />' +
|
|
'<nodes ID="8888" LABEL="Who am I?" COLORVALUE="green" COLORLABEL="unspecified" SIZEVALUE="2000" ' +
|
|
'LINK="https://github.com/ogobrecht/d3-force-apex-plugin/wiki/API-Reference#nodelinktarget" ' +
|
|
'INFOSTRING="This is a good question. Think about it." />' +
|
|
'<nodes ID="9999" LABEL="Where I am?" COLORVALUE="#f00" COLORLABEL="unspecified" SIZEVALUE="1000" ' +
|
|
'LINK="https://github.com/ogobrecht/d3-force-apex-plugin/wiki/API-Reference#nodelinktarget" ' +
|
|
'INFOSTRING="This is a good question. What do you think?" />' +
|
|
'<links FROMID="7839" TOID="7839" STYLE="dotted" COLOR="blue" ' +
|
|
'INFOSTRING="This is a self link (same source and target node) rendered along a path with the STYLE ' +
|
|
'attribute set to dotted and COLOR attribute set to blue." />' +
|
|
'<links FROMID="7698" TOID="7839" STYLE="dashed" />' +
|
|
'<links FROMID="7782" TOID="7839" STYLE="dashed" COLOR="red" INFOSTRING="This is a link with the STYLE ' +
|
|
'attribute set to dashed and COLOR attribute set to red." />' +
|
|
'<links FROMID="7566" TOID="7839" STYLE="dashed" />' +
|
|
'<links FROMID="7788" TOID="7566" STYLE="solid" />' +
|
|
'<links FROMID="7902" TOID="7566" STYLE="solid" />' +
|
|
'<links FROMID="7369" TOID="7902" STYLE="solid" />' +
|
|
'<links FROMID="7499" TOID="7698" STYLE="solid" />' +
|
|
'<links FROMID="7521" TOID="7698" STYLE="solid" />' +
|
|
'<links FROMID="7654" TOID="7698" STYLE="solid" />' +
|
|
'<links FROMID="7844" TOID="7698" STYLE="solid" />' +
|
|
'<links FROMID="7876" TOID="7788" STYLE="solid" />' +
|
|
'<links FROMID="7900" TOID="7698" STYLE="solid" />' +
|
|
'<links FROMID="7934" TOID="7782" STYLE="solid" />' +
|
|
'</data>';
|
|
/* jshint +W110 */
|
|
|
|
// check user agent: http://stackoverflow.com/questions/16135814/check-for-ie-10
|
|
v.status.userAgent = navigator.userAgent;
|
|
v.status.userAgentIe9To11 = false;
|
|
// Hello IE 9 - 11
|
|
if (navigator.appVersion.indexOf("MSIE 9") !== -1 ||
|
|
navigator.appVersion.indexOf("MSIE 10") !== -1 ||
|
|
v.status.userAgent.indexOf("Trident") !== -1 && v.status.userAgent.indexOf("rv:11") !== -1) {
|
|
v.status.userAgentIe9To11 = true;
|
|
v.tools.logError("Houston, we have a problem - user agent is IE 9, 10 or 11 - we have to provide a fix " +
|
|
"for markers: " +
|
|
"http://stackoverflow.com/questions/15588478/internet-explorer-10-not-showing-svg-path-d3-js-graph");
|
|
}
|
|
|
|
}; // --> END v.main.setupConfiguration
|
|
|
|
/*******************************************************************************************************************
|
|
* MAIN: SETUP DOM
|
|
*/
|
|
v.main.setupDom = function() {
|
|
|
|
// create reference to body
|
|
v.dom.body = d3.select("body");
|
|
|
|
// create DOM container element, if not existing (if we have an APEX context, it is already created from the
|
|
// APEX engine )
|
|
if (document.querySelector("#" + v.dom.containerId) === null) {
|
|
v.dom.container = v.dom.body.append("div")
|
|
.attr("id", v.dom.containerId);
|
|
} else {
|
|
v.dom.container = d3.select("#" + v.dom.containerId);
|
|
d3.selectAll("#" + v.dom.containerId + "_tooltip, #" + v.dom.containerId + "_customizing").remove();
|
|
}
|
|
|
|
// create SVG element, if not existing (if we have an APEX context, it is already created from the APEX plugin )
|
|
if (document.querySelector("#" + v.dom.containerId + " svg") === null) {
|
|
v.dom.svg = v.dom.container.append("svg");
|
|
} else {
|
|
v.dom.svg = d3.select("#" + v.dom.containerId + " svg");
|
|
d3.selectAll("#" + v.dom.containerId + " svg *").remove();
|
|
}
|
|
|
|
v.dom.svgParent = d3.select(v.dom.svg.node().parentNode);
|
|
if (v.conf.setDomParentPaddingToZero) {
|
|
v.dom.svgParent.style("padding", "0");
|
|
}
|
|
|
|
// configure SVG element
|
|
v.dom.svg
|
|
.attr("class", "net_gobrechts_d3_force")
|
|
.classed("border", v.conf.showBorder)
|
|
.attr("width", v.conf.width)
|
|
.attr("height", v.conf.height);
|
|
|
|
// calculate width of SVG parent
|
|
v.dom.containerWidth = v.tools.getSvgParentInnerWidth();
|
|
if (v.conf.useDomParentWidth) {
|
|
v.dom.svg.attr("width", v.dom.containerWidth);
|
|
}
|
|
|
|
// create definitions element inside the SVG element
|
|
v.dom.defs = v.dom.svg.append("defs");
|
|
|
|
// create overlay element to fetch events for lasso & zoom
|
|
v.dom.graphOverlay = v.dom.svg.append("g").attr("class", "graphOverlay");
|
|
|
|
// create element for resizing the overlay g element
|
|
v.dom.graphOverlaySizeHelper = v.dom.graphOverlay.append("rect").attr("class", "graphOverlaySizeHelper");
|
|
|
|
// create graph group element for zoom and pan
|
|
v.dom.graph = v.dom.graphOverlay.append("g").attr("class", "graph");
|
|
|
|
// create legend group element
|
|
v.dom.legend = v.dom.svg.append("g").attr("class", "legend");
|
|
|
|
// create loading indicator
|
|
v.dom.loading = v.dom.svg.append("svg:g")
|
|
.attr("class", "loading")
|
|
.style("display", "none");
|
|
v.dom.loadingRect = v.dom.loading
|
|
.append("svg:rect")
|
|
.attr("width", v.tools.getGraphWidth())
|
|
.attr("height", v.conf.height);
|
|
v.dom.loadingText = v.dom.loading
|
|
.append("svg:text")
|
|
.attr("x", v.tools.getGraphWidth() / 2)
|
|
.attr("y", v.conf.height / 2)
|
|
.text("Loading...");
|
|
|
|
// create marker definitions
|
|
v.dom.defs
|
|
.append("svg:marker")
|
|
.attr("id", v.dom.containerId + "_highlighted")
|
|
.attr("class", "highlighted")
|
|
.attr("viewBox", "0 0 10 10")
|
|
.attr("refX", 10)
|
|
.attr("refY", 5)
|
|
.attr("markerWidth", 5)
|
|
.attr("markerHeight", 5)
|
|
.attr("orient", "auto")
|
|
.attr("markerUnits", "strokeWidth")
|
|
.append("svg:path")
|
|
.attr("d", "M0,0 L10,5 L0,10");
|
|
|
|
v.dom.defs
|
|
.append("svg:marker")
|
|
.attr("id", v.dom.containerId + "_normal")
|
|
.attr("class", "normal")
|
|
.attr("viewBox", "0 0 10 10")
|
|
.attr("refX", 10)
|
|
.attr("refY", 5)
|
|
.attr("markerWidth", 5)
|
|
.attr("markerHeight", 5)
|
|
.attr("orient", "auto")
|
|
.attr("markerUnits", "strokeWidth")
|
|
.append("svg:path")
|
|
.attr("d", "M0,0 L10,5 L0,10");
|
|
|
|
// create tooltip container
|
|
if (document.querySelector("#" + v.dom.containerId + "_tooltip") === null) {
|
|
v.dom.tooltip = v.dom.body.append("div")
|
|
.attr("id", v.dom.containerId + "_tooltip")
|
|
.attr("class", "net_gobrechts_d3_force_tooltip")
|
|
.style("top", "0px")
|
|
.style("left", "0px");
|
|
} else {
|
|
v.dom.tooltip = d3.select("#" + v.dom.containerId + "_tooltip");
|
|
}
|
|
|
|
}; // --> END v.main.setupDom
|
|
|
|
|
|
/*******************************************************************************************************************
|
|
* MAIN: SETUP FUNCTION REFERENCES
|
|
*/
|
|
v.main.setupFunctionReferences = function() {
|
|
|
|
// create force reference
|
|
v.main.force = d3.layout.force()
|
|
.on("start", function() {
|
|
v.tools.log("Force started.");
|
|
if (v.status.customize && v.dom.customizePositions) {
|
|
v.dom.customizePositions.text("Force started - wait for end event to show positions...");
|
|
}
|
|
v.status.forceTickCounter = 0;
|
|
v.status.forceStartTime = new Date().getTime();
|
|
v.status.forceRunning = true;
|
|
})
|
|
.on("tick", function() {
|
|
// hello IE 9 - 11:
|
|
// http://stackoverflow.com/questions/15588478/internet-explorer-10-not-showing-svg-path-d3-js-graph
|
|
if (v.status.userAgentIe9To11 && v.conf.showLinkDirection) {
|
|
v.main.links.each(function() {
|
|
this.parentNode.insertBefore(this, this);
|
|
});
|
|
v.main.selfLinks.each(function() {
|
|
this.parentNode.insertBefore(this, this);
|
|
});
|
|
}
|
|
v.main.selfLinks
|
|
.attr("transform", function(l) {
|
|
return "translate(" + l.source.x + "," + l.source.y + ")";
|
|
});
|
|
v.main.links
|
|
.attr("x1", function(l) {
|
|
return v.tools.adjustSourceX(l);
|
|
})
|
|
.attr("y1", function(l) {
|
|
return v.tools.adjustSourceY(l);
|
|
})
|
|
.attr("x2", function(l) {
|
|
return v.tools.adjustTargetX(l);
|
|
})
|
|
.attr("y2", function(l) {
|
|
return v.tools.adjustTargetY(l);
|
|
});
|
|
if (v.conf.showLabels) {
|
|
v.main.labels
|
|
.attr("x", function(l) {
|
|
return l.x;
|
|
})
|
|
.attr("y", function(l) {
|
|
return l.y - l.radius - v.conf.labelDistance;
|
|
});
|
|
v.main.labelPaths
|
|
.attr("transform", function(n) {
|
|
return "translate(" + n.x + "," + n.y + ")";
|
|
});
|
|
}
|
|
v.main.nodes
|
|
.attr("cx", function(n) {
|
|
return n.x;
|
|
})
|
|
.attr("cy", function(n) {
|
|
return n.y;
|
|
});
|
|
v.status.forceTickCounter += 1;
|
|
})
|
|
.on("end", function() {
|
|
if (v.conf.showLabels && v.conf.preventLabelOverlappingOnForceEnd) {
|
|
v.data.simulatedAnnealingLabels = [];
|
|
v.data.simulatedAnnealingAnchors = [];
|
|
v.main.labels.each(function(l, i) {
|
|
v.data.simulatedAnnealingLabels[i] = {
|
|
width: this.getBBox().width,
|
|
height: this.getBBox().height,
|
|
x: l.x,
|
|
y: l.y - l.radius - v.conf.labelDistance
|
|
};
|
|
});
|
|
v.main.nodes.filter(function(n) {
|
|
return !n.LABELCIRCULAR && !v.conf.labelsCircular;
|
|
}).each(function(n, i) {
|
|
v.data.simulatedAnnealingAnchors[i] = {
|
|
x: n.x,
|
|
y: n.y - n.radius - v.conf.labelDistance, // set anchors to the same positions as the label
|
|
r: 0.5 //fake radius for labeler plugin, because our labels are already outside of the nodes
|
|
};
|
|
});
|
|
v.lib.labelerPlugin()
|
|
.label(v.data.simulatedAnnealingLabels)
|
|
.anchor(v.data.simulatedAnnealingAnchors)
|
|
.width(v.tools.getGraphWidth())
|
|
.height(v.conf.height)
|
|
.start(v.conf.labelPlacementIterations);
|
|
v.main.labels.each(function(l, i) {
|
|
d3.select(this)
|
|
.transition()
|
|
.duration(800)
|
|
.attr("x", v.data.simulatedAnnealingLabels[i].x)
|
|
.attr("y", v.data.simulatedAnnealingLabels[i].y);
|
|
});
|
|
}
|
|
v.status.forceRunning = false;
|
|
var milliseconds = new Date().getTime() - v.status.forceStartTime;
|
|
var seconds = (milliseconds / 1000).toFixed(1);
|
|
var ticksPerSecond = Math.round(v.status.forceTickCounter / (milliseconds / 1000));
|
|
var millisecondsPerTick = Math.round(milliseconds / v.status.forceTickCounter);
|
|
if (v.status.customize && v.dom.customizePositions) {
|
|
v.dom.customizePositions.text(JSON.stringify(graph.positions()));
|
|
}
|
|
v.tools.log("Force ended.");
|
|
v.tools.log(seconds + " seconds, " + v.status.forceTickCounter + " ticks to cool down (" +
|
|
ticksPerSecond + " ticks/s, " + millisecondsPerTick + " ms/tick).");
|
|
});
|
|
|
|
// create drag reference
|
|
v.main.drag = v.main.force.drag();
|
|
|
|
// create lasso reference
|
|
v.main.lasso = v.lib.lassoPlugin()
|
|
.closePathDistance(100) // max distance for the lasso loop to be closed
|
|
.closePathSelect(true) // can items be selected by closing the path?
|
|
.hoverSelect(true) // can items by selected by hovering over them?
|
|
.area(v.dom.graphOverlay) // area where the lasso can be started
|
|
.pathContainer(v.dom.svg); // Container for the path
|
|
|
|
// create zoom reference
|
|
v.main.zoom = d3.behavior.zoom();
|
|
|
|
}; // --> END v.main.setupFunctionReferences
|
|
|
|
|
|
/*******************************************************************************************************************
|
|
* HELPER FUNCTIONS
|
|
*/
|
|
|
|
// helper to check boolean values
|
|
v.tools.parseBool = function(value) {
|
|
switch (String(value).trim().toLowerCase()) {
|
|
case "true":
|
|
case "yes":
|
|
case "1":
|
|
return true;
|
|
case "false":
|
|
case "no":
|
|
case "0":
|
|
case "":
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// parse XML string to XML
|
|
v.tools.parseXml = function(xml) {
|
|
var dom = null;
|
|
if (xml) {
|
|
if (window.DOMParser) {
|
|
try {
|
|
dom = (new DOMParser()).parseFromString(xml, "text/xml");
|
|
} catch (e) {
|
|
dom = null;
|
|
v.tools.logError("DOMParser - unable to parse XML: " + e.message);
|
|
}
|
|
} else if (window.ActiveXObject) {
|
|
try {
|
|
dom = new ActiveXObject("Microsoft.XMLDOM");
|
|
dom.async = false;
|
|
// parse error ...
|
|
if (!dom.loadXML(xml)) {
|
|
v.tools.logError("Microsoft.XMLDOM - unable to parse XML: " + dom.parseError.reason +
|
|
dom.parseError.srcText);
|
|
}
|
|
} catch (e) {
|
|
dom = null;
|
|
v.tools.logError("Microsoft.XMLDOM - unable to parse XML: " + e.message);
|
|
}
|
|
}
|
|
}
|
|
return dom;
|
|
};
|
|
|
|
// convert XML to JSON: modified version of http://davidwalsh.name/convert-xml-json
|
|
v.tools.xmlToJson = function(xml) {
|
|
var obj = null, subobj, item, subItem, nodeName, attribute;
|
|
var convertItemToJson = function(item) {
|
|
subobj = {};
|
|
if (item.attributes.length > 0) {
|
|
for (var i = 0; i < item.attributes.length; i++) {
|
|
attribute = item.attributes.item(i);
|
|
subobj[attribute.nodeName] = attribute.nodeValue;
|
|
}
|
|
}
|
|
if (item.hasChildNodes()) {
|
|
for(var j = 0; j < item.childNodes.length; j++) {
|
|
subItem = item.childNodes.item(j);
|
|
// check, if subItem has minimum one child (hopefully a textnode) inside
|
|
if (subItem.hasChildNodes()) { subobj[subItem.nodeName] = subItem.childNodes.item(0).nodeValue; }
|
|
else { subobj[subItem.nodeName] = ""; }
|
|
}
|
|
}
|
|
return subobj;
|
|
};
|
|
if (xml) {
|
|
obj = {};
|
|
obj.data = {};
|
|
obj.data.nodes = [];
|
|
obj.data.links = [];
|
|
if (xml.childNodes.item(0).hasChildNodes()) {
|
|
for(var i = 0; i < xml.childNodes.item(0).childNodes.length; i++) {
|
|
subobj = null;
|
|
item = xml.childNodes.item(0).childNodes.item(i);
|
|
nodeName = item.nodeName;
|
|
if (nodeName === "nodes" || nodeName === "node") {
|
|
obj.data.nodes.push(convertItemToJson(item));
|
|
}
|
|
else if (nodeName === "links" || nodeName === "link") {
|
|
obj.data.links.push(convertItemToJson(item));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return obj;
|
|
};
|
|
|
|
// get inner width for the SVG parents element
|
|
v.tools.getSvgParentInnerWidth = function() {
|
|
return parseInt(v.dom.svgParent.style("width")) -
|
|
parseInt(v.dom.svgParent.style("padding-left")) -
|
|
parseInt(v.dom.svgParent.style("padding-right")) -
|
|
(v.dom.svg.style("border-width") ? parseInt(v.dom.svg.style("border-width")) : 1) * 2;
|
|
};
|
|
|
|
// helper function to get effective graph width
|
|
v.tools.getGraphWidth = function() {
|
|
return (v.conf.useDomParentWidth ? v.dom.containerWidth : v.conf.width);
|
|
};
|
|
|
|
// log function for debug mode
|
|
v.tools.log = function(message, omitDebugPrefix) {
|
|
if (v.conf.debug) {
|
|
if (omitDebugPrefix) {
|
|
console.log(message);
|
|
} else {
|
|
console.log(v.status.debugPrefix + message);
|
|
}
|
|
}
|
|
if (v.status.customize && v.dom.customizeLog) {
|
|
v.dom.customizeLog.text(message + "\n" + v.dom.customizeLog.text());
|
|
}
|
|
};
|
|
|
|
// log error function
|
|
v.tools.logError = function(message) {
|
|
console.log(v.status.debugPrefix + "ERROR: " + message);
|
|
if (v.status.customize && v.dom.customizeLog) {
|
|
v.dom.customizeLog.text("ERROR: " + message + "\n" + v.dom.customizeLog.text());
|
|
}
|
|
};
|
|
|
|
// trigger APEX events, if we have an APEX context
|
|
v.tools.triggerApexEvent = function(domNode, event, data) {
|
|
if (v.status.apexPluginId) {
|
|
apex.event.trigger(domNode, event, data);
|
|
}
|
|
};
|
|
|
|
// helper function to calculate node radius from "SIZEVALUE" attribute
|
|
v.tools.setRadiusFunction = function() {
|
|
v.tools.radius = d3.scale.sqrt()
|
|
.range([v.conf.minNodeRadius, v.conf.maxNodeRadius])
|
|
.domain(d3.extent(v.data.nodes, function(n) {
|
|
return parseFloat(n.SIZEVALUE);
|
|
}));
|
|
};
|
|
|
|
// helper function to calculate node fill color from COLORVALUE attribute
|
|
v.tools.setColorFunction = function() {
|
|
if (v.conf.colorScheme === "color20") {
|
|
v.tools.color = d3.scale.category20();
|
|
} else if (v.conf.colorScheme === "color20b") {
|
|
v.tools.color = d3.scale.category20b();
|
|
} else if (v.conf.colorScheme === "color20c") {
|
|
v.tools.color = d3.scale.category20c();
|
|
} else if (v.conf.colorScheme === "color10") {
|
|
v.tools.color = d3.scale.category10();
|
|
} else if (v.conf.colorScheme === "direct") {
|
|
v.tools.color = function(d) {
|
|
return d;
|
|
};
|
|
} else {
|
|
v.conf.colorScheme = "color20";
|
|
v.tools.color = d3.scale.category20();
|
|
}
|
|
};
|
|
|
|
// check, if two nodes are neighbors
|
|
v.tools.neighboring = function(a, b) {
|
|
return (v.data.neighbors.indexOf(a.ID + ":" + b.ID) > -1 ||
|
|
v.data.neighbors.indexOf(b.ID + ":" + a.ID) > -1);
|
|
};
|
|
|
|
// get nearest grid position
|
|
v.tools.getNearestGridPosition = function(currentPos, maxPos) {
|
|
var offset, position;
|
|
// no size limit for calculated positions, if zoomMode is set to true
|
|
if (v.conf.zoomMode) {
|
|
offset = currentPos % v.conf.gridSize;
|
|
position = (offset > v.conf.gridSize / 2 ? currentPos - offset + v.conf.gridSize : currentPos - offset);
|
|
}
|
|
// size limit for calculated positions is SVG size, if zoomMode is set to false
|
|
else {
|
|
if (currentPos >= maxPos) {
|
|
offset = maxPos % v.conf.gridSize;
|
|
position = maxPos - offset;
|
|
if (position === maxPos) {
|
|
position = position - v.conf.gridSize;
|
|
}
|
|
} else if (currentPos <= v.conf.gridSize / 2) {
|
|
position = v.conf.gridSize;
|
|
} else {
|
|
offset = currentPos % v.conf.gridSize;
|
|
position = (offset > v.conf.gridSize / 2 ? currentPos - offset + v.conf.gridSize : currentPos - offset);
|
|
if (position >= maxPos) {
|
|
position = position - v.conf.gridSize;
|
|
}
|
|
}
|
|
}
|
|
return position;
|
|
};
|
|
|
|
// adjust link x/y
|
|
v.tools.adjustSourceX = function(l) {
|
|
return l.source.x + Math.cos(v.tools.calcAngle(l)) * (l.source.radius);
|
|
};
|
|
v.tools.adjustSourceY = function(l) {
|
|
return l.source.y + Math.sin(v.tools.calcAngle(l)) * (l.source.radius);
|
|
};
|
|
v.tools.adjustTargetX = function(l) {
|
|
return l.target.x - Math.cos(v.tools.calcAngle(l)) * (l.target.radius);
|
|
};
|
|
v.tools.adjustTargetY = function(l) {
|
|
return l.target.y - Math.sin(v.tools.calcAngle(l)) * (l.target.radius);
|
|
};
|
|
v.tools.calcAngle = function(l) {
|
|
return Math.atan2(l.target.y - l.source.y, l.target.x - l.source.x);
|
|
};
|
|
|
|
// create a path for self links
|
|
v.tools.getSelfLinkPath = function(l) {
|
|
var ri = l.source.radius;
|
|
var ro = l.source.radius + v.conf.selfLinkDistance;
|
|
var x = 0; // we position the path later with transform/translate
|
|
var y = 0;
|
|
var pathStart = {
|
|
"source": {
|
|
"x": 0,
|
|
"y": 0,
|
|
"radius": ri
|
|
},
|
|
"target": {
|
|
"x": (x + ro / 2),
|
|
"y": (y + ro),
|
|
"radius": ri
|
|
}
|
|
};
|
|
var pathEnd = {
|
|
"source": {
|
|
"x": (x - ro / 2),
|
|
"y": (y + ro),
|
|
"radius": ri
|
|
},
|
|
"target": {
|
|
"x": x,
|
|
"y": y,
|
|
"radius": ri
|
|
}
|
|
};
|
|
var path = "M" + v.tools.adjustSourceX(pathStart) + "," + v.tools.adjustSourceY(pathStart);
|
|
path += " L" + (x + ro / 2) + "," + (y + ro);
|
|
path += " A" + ro + "," + ro + " 0 0,1 " + (x - ro / 2) + "," + (y + ro);
|
|
path += " L" + v.tools.adjustTargetX(pathEnd) + "," + v.tools.adjustTargetY(pathEnd);
|
|
return path;
|
|
};
|
|
|
|
// create a path for labels - example: d="M100,100 a20,20 0 0,1 40,0"
|
|
v.tools.getLabelPath = function(n) {
|
|
var r = n.radius + v.conf.labelDistance;
|
|
var x = 0; // we position the path later with transform/translate
|
|
var y = 0;
|
|
var path = "M" + (x - r) + "," + y;
|
|
//path += " a" + r + "," + r + " 0 0,1 " + (r * 2) + ",0";
|
|
path += " a" + r + "," + r + " 0 0,1 " + (r * 2) + ",0";
|
|
path += " a" + r + "," + r + " 0 0,1 -" + (r * 2) + ",0";
|
|
return path;
|
|
};
|
|
|
|
// open link function
|
|
v.tools.openLink = function(node) {
|
|
var win;
|
|
if (v.conf.nodeLinkTarget === "none") {
|
|
window.location.assign(node.LINK);
|
|
} else if (v.conf.nodeLinkTarget === "nodeID") {
|
|
win = window.open(node.LINK, node.ID);
|
|
win.focus();
|
|
} else if (v.conf.nodeLinkTarget === "domContainerID") {
|
|
win = window.open(node.LINK, v.dom.containerId);
|
|
win.focus();
|
|
} else {
|
|
win = window.open(node.LINK, v.conf.nodeLinkTarget);
|
|
win.focus();
|
|
}
|
|
};
|
|
|
|
v.tools.applyConfigurationObject = function(confObject) {
|
|
var key;
|
|
for (key in confObject) {
|
|
if (confObject.hasOwnProperty(key) &&
|
|
v.conf.hasOwnProperty(key) &&
|
|
confObject[key] !== v.conf[key]) {
|
|
graph[key](confObject[key]);
|
|
}
|
|
}
|
|
};
|
|
|
|
// http://stackoverflow.com/questions/13713528/how-to-disable-pan-for-d3-behavior-zoom
|
|
// http://stackoverflow.com/questions/11786023/how-to-disable-double-click-zoom-for-d3-behavior-zoom
|
|
// zoom event proxy
|
|
v.tools.zoomEventProxy = function(fn) {
|
|
return function() {
|
|
if (
|
|
(!v.conf.dragMode || v.conf.dragMode && d3.event.target.tagName !== "circle") &&
|
|
v.conf.zoomMode &&
|
|
(!d3.event.altKey && !d3.event.shiftKey)
|
|
) {
|
|
fn.apply(this, arguments);
|
|
}
|
|
};
|
|
};
|
|
// lasso event proxy
|
|
v.tools.lassoEventProxy = function(fn) {
|
|
return function() {
|
|
if (
|
|
(!v.conf.dragMode || d3.event.target.tagName !== "circle") &&
|
|
v.conf.lassoMode &&
|
|
(!v.conf.zoomMode || d3.event.altKey || d3.event.shiftKey)
|
|
) {
|
|
fn.apply(this, arguments);
|
|
}
|
|
};
|
|
};
|
|
|
|
// show tooltip
|
|
v.tools.showTooltip = function(text) {
|
|
var position;
|
|
v.dom.tooltip.html(text).style("display", "block");
|
|
if (v.conf.tooltipPosition === "svgTopLeft") {
|
|
position = v.tools.getOffsetRect(v.dom.svg.node());
|
|
v.dom.tooltip
|
|
.style("top", position.top +
|
|
(v.dom.svg.style("border-width") ? parseInt(v.dom.svg.style("border-width")) : 1) +
|
|
"px")
|
|
.style("left", position.left +
|
|
(v.dom.svg.style("border-width") ? parseInt(v.dom.svg.style("border-width")) : 1) +
|
|
"px");
|
|
} else if (v.conf.tooltipPosition === "svgTopRight") {
|
|
position = v.tools.getOffsetRect(v.dom.svg.node());
|
|
v.dom.tooltip
|
|
.style("top", position.top +
|
|
parseInt((v.dom.svg.style("border-width") ? parseInt(v.dom.svg.style("border-width")) : 1)) +
|
|
"px")
|
|
.style("left", position.left +
|
|
parseInt(v.dom.svg.style("width")) +
|
|
parseInt((v.dom.svg.style("border-width") ? parseInt(v.dom.svg.style("border-width")) : 1)) -
|
|
parseInt(v.dom.tooltip.style("width")) -
|
|
2 * parseInt(
|
|
(v.dom.tooltip.style("border-width") ? parseInt(v.dom.tooltip.style("border-width")) : 0)
|
|
) -
|
|
parseInt(v.dom.tooltip.style("padding-left")) -
|
|
parseInt(v.dom.tooltip.style("padding-right")) +
|
|
"px");
|
|
} else {
|
|
v.dom.tooltip
|
|
.style("left", d3.event.pageX + 10 + "px")
|
|
.style("top", d3.event.pageY + "px");
|
|
}
|
|
};
|
|
|
|
// hide tooltip
|
|
v.tools.hideTooltip = function() {
|
|
v.dom.tooltip.style("display", "none");
|
|
};
|
|
|
|
// on link click function
|
|
v.tools.onLinkClick = function(link) {
|
|
if (d3.event.defaultPrevented) { // ignore drag
|
|
return null;
|
|
} else {
|
|
v.tools.log("Event link_click triggered.");
|
|
v.tools.triggerApexEvent(this, "net_gobrechts_d3_force_linkclick", link);
|
|
if (typeof(v.conf.onLinkClickFunction) === "function") {
|
|
v.conf.onLinkClickFunction.call(this, d3.event, link);
|
|
}
|
|
}
|
|
};
|
|
// get marker URL
|
|
v.tools.getMarkerUrl = function(l) {
|
|
if (v.conf.showLinkDirection) {
|
|
return "url(#" + v.dom.containerId + "_" + (l.COLOR ? l.COLOR : "normal") + ")";
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
v.tools.getMarkerUrlHighlighted = function() {
|
|
if (v.conf.showLinkDirection) {
|
|
return "url(#" + v.dom.containerId + "_highlighted)";
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// on link mouseenter function
|
|
v.tools.onLinkMouseenter = function(link) {
|
|
if (v.conf.showTooltips && link.INFOSTRING) {
|
|
v.tools.showTooltip(link.INFOSTRING);
|
|
}
|
|
};
|
|
|
|
// on link mouseleave function
|
|
v.tools.onLinkMouseleave = function() {
|
|
if (v.conf.showTooltips) {
|
|
v.tools.hideTooltip();
|
|
}
|
|
};
|
|
|
|
// on node mouse enter function
|
|
v.tools.onNodeMouseenter = function(node) {
|
|
v.main.nodes.classed("highlighted", function(n) {
|
|
return v.tools.neighboring(n, node);
|
|
});
|
|
v.main.links
|
|
.classed("highlighted", function(l) {
|
|
return l.source.ID === node.ID || l.target.ID === node.ID;
|
|
})
|
|
.style("marker-end", function(l) {
|
|
if (l.source.ID === node.ID || l.target.ID === node.ID) {
|
|
return v.tools.getMarkerUrlHighlighted(l);
|
|
} else {
|
|
return v.tools.getMarkerUrl(l);
|
|
}
|
|
});
|
|
v.main.selfLinks
|
|
.classed("highlighted", function(l) {
|
|
return l.FROMID === node.ID;
|
|
})
|
|
.style("marker-end", function(l) {
|
|
if (l.source.ID === node.ID || l.target.ID === node.ID) {
|
|
return v.tools.getMarkerUrlHighlighted(l);
|
|
} else {
|
|
return v.tools.getMarkerUrl(l);
|
|
}
|
|
});
|
|
if (v.conf.showLabels) {
|
|
v.main.labels.classed("highlighted", function (l) {
|
|
return l.ID === node.ID;
|
|
});
|
|
v.main.labelsCircular.classed("highlighted", function (l) {
|
|
return l.ID === node.ID;
|
|
});
|
|
}
|
|
d3.select(this).classed("highlighted", true);
|
|
v.tools.log("Event node_mouseenter triggered.");
|
|
v.tools.triggerApexEvent(this, "net_gobrechts_d3_force_mouseenter", node);
|
|
if (typeof(v.conf.onNodeMouseenterFunction) === "function") {
|
|
v.conf.onNodeMouseenterFunction.call(this, d3.event, node);
|
|
}
|
|
if (v.conf.showTooltips && node.INFOSTRING) {
|
|
v.tools.showTooltip(node.INFOSTRING);
|
|
}
|
|
};
|
|
|
|
// on node mouse leave function
|
|
v.tools.onNodeMouseleave = function(node) {
|
|
v.main.nodes.classed("highlighted", false);
|
|
v.main.links
|
|
.classed("highlighted", false)
|
|
.style("marker-end", v.tools.getMarkerUrl);
|
|
v.main.selfLinks
|
|
.classed("highlighted", false)
|
|
.style("marker-end", v.tools.getMarkerUrl);
|
|
if (v.conf.showLabels) {
|
|
v.main.labels.classed("highlighted", false);
|
|
v.main.labelsCircular.classed("highlighted", false);
|
|
}
|
|
v.tools.log("Event node_mouseleave triggered.");
|
|
v.tools.triggerApexEvent(this, "net_gobrechts_d3_force_mouseleave", node);
|
|
if (typeof(v.conf.onNodeMouseleaveFunction) === "function") {
|
|
v.conf.onNodeMouseleaveFunction.call(this, d3.event, node);
|
|
}
|
|
if (v.conf.showTooltips) {
|
|
v.tools.hideTooltip();
|
|
}
|
|
};
|
|
|
|
// on node click function
|
|
v.tools.onNodeClick = function(node) {
|
|
if (d3.event.defaultPrevented) { // ignore drag
|
|
return null;
|
|
} else {
|
|
if (node.LINK && v.conf.nodeEventToOpenLink === "click") {
|
|
v.tools.openLink(node);
|
|
}
|
|
if (v.conf.nodeEventToStopPinMode === "click") {
|
|
d3.select(this).classed("fixed", node.fixed = 0);
|
|
}
|
|
v.tools.log("Event node_click triggered.");
|
|
v.tools.triggerApexEvent(this, "net_gobrechts_d3_force_click", node);
|
|
if (typeof(v.conf.onNodeClickFunction) === "function") {
|
|
v.conf.onNodeClickFunction.call(this, d3.event, node);
|
|
}
|
|
}
|
|
};
|
|
|
|
// on node double click function
|
|
v.tools.onNodeDblclick = function(node) {
|
|
if (node.LINK && v.conf.nodeEventToOpenLink === "dblclick") {
|
|
v.tools.openLink(node);
|
|
}
|
|
if (v.conf.nodeEventToStopPinMode === "dblclick") {
|
|
d3.select(this).classed("fixed", node.fixed = 0);
|
|
}
|
|
v.tools.log("Event node_dblclick triggered.");
|
|
v.tools.triggerApexEvent(this, "net_gobrechts_d3_force_dblclick", node);
|
|
if (typeof(v.conf.onNodeDblclickFunction) === "function") {
|
|
v.conf.onNodeDblclickFunction.call(this, d3.event, node);
|
|
}
|
|
};
|
|
|
|
// on node contextmenu function
|
|
v.tools.onNodeContextmenu = function(node) {
|
|
if (v.conf.onNodeContextmenuPreventDefault) {
|
|
d3.event.preventDefault();
|
|
}
|
|
if (node.LINK && v.conf.nodeEventToOpenLink === "contextmenu") {
|
|
v.tools.openLink(node);
|
|
}
|
|
if (v.conf.nodeEventToStopPinMode === "contextmenu") {
|
|
d3.select(this).classed("fixed", node.fixed = 0);
|
|
}
|
|
v.tools.log("Event node_contextmenu triggered.");
|
|
v.tools.triggerApexEvent(this, "net_gobrechts_d3_force_contextmenu", node);
|
|
if (typeof(v.conf.onNodeContextmenuFunction) === "function") {
|
|
v.conf.onNodeContextmenuFunction.call(this, d3.event, node);
|
|
}
|
|
};
|
|
|
|
// on lasso start function
|
|
v.tools.onLassoStart = function(nodes) {
|
|
var data = {};
|
|
data.numberOfSelectedNodes = 0;
|
|
data.idsOfSelectedNodes = null;
|
|
data.numberOfNodes = nodes.size();
|
|
data.nodes = nodes;
|
|
v.tools.log("Event lasso_start triggered.");
|
|
v.tools.triggerApexEvent(document.querySelector("#" + v.dom.containerId),
|
|
"net_gobrechts_d3_force_lassostart",
|
|
data
|
|
);
|
|
if (typeof(v.conf.onLassoStartFunction) === "function") {
|
|
v.conf.onLassoStartFunction.call(v.dom.svg, d3.event, data);
|
|
}
|
|
};
|
|
|
|
// on lasso end function
|
|
v.tools.onLassoEnd = function(nodes) {
|
|
var data = {};
|
|
data.numberOfSelectedNodes = 0;
|
|
data.idsOfSelectedNodes = "";
|
|
data.numberOfNodes = nodes.size();
|
|
data.nodes = nodes;
|
|
nodes.each(function(n) {
|
|
if (n.selected) {
|
|
data.idsOfSelectedNodes += (n.ID + ":");
|
|
data.numberOfSelectedNodes++;
|
|
}
|
|
});
|
|
data.idsOfSelectedNodes =
|
|
(data.idsOfSelectedNodes.length > 0 ?
|
|
data.idsOfSelectedNodes.substr(0, data.idsOfSelectedNodes.length - 1) :
|
|
null);
|
|
v.tools.log("Event lasso_end triggered.");
|
|
v.tools.triggerApexEvent(document.querySelector("#" + v.dom.containerId),
|
|
"net_gobrechts_d3_force_lassoend", data);
|
|
if (typeof(v.conf.onLassoEndFunction) === "function") {
|
|
v.conf.onLassoEndFunction.call(v.dom.svg, d3.event, data);
|
|
}
|
|
};
|
|
|
|
// get offset for an element relative to the document: http://javascript.info/tutorial/coordinates
|
|
v.tools.getOffsetRect = function(elem) {
|
|
var box = elem.getBoundingClientRect();
|
|
var body = document.body;
|
|
var docElem = document.documentElement;
|
|
var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop;
|
|
var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft;
|
|
var clientTop = docElem.clientTop || body.clientTop || 0;
|
|
var clientLeft = docElem.clientLeft || body.clientLeft || 0;
|
|
var top = box.top + scrollTop - clientTop;
|
|
var left = box.left + scrollLeft - clientLeft;
|
|
return {
|
|
top: Math.round(top),
|
|
left: Math.round(left)
|
|
};
|
|
};
|
|
|
|
// create legend
|
|
v.tools.createLegend = function() {
|
|
v.data.distinctNodeColorValues.forEach(function(colorString, i) {
|
|
var color = colorString.split(";");
|
|
v.dom.legend
|
|
.append("circle")
|
|
.attr("cx", 11)
|
|
.attr("cy", v.conf.height - ((i + 1) * 14 - 3))
|
|
.attr("r", 6)
|
|
.attr("fill", v.tools.color(color[1]));
|
|
v.dom.legend
|
|
.append("text")
|
|
.attr("x", 21)
|
|
.attr("y", v.conf.height - ((i + 1) * 14 - 6))
|
|
.text((color[0] ? color[0] : color[1]));
|
|
});
|
|
};
|
|
|
|
// remove legend
|
|
v.tools.removeLegend = function() {
|
|
v.dom.legend.selectAll("*").remove();
|
|
};
|
|
|
|
// write conf object into customization wizard
|
|
v.tools.writeConfObjectIntoWizard = function() {
|
|
if (v.status.customize) {
|
|
v.dom.customizeConfObject.text(JSON.stringify(graph.optionsCustomizationWizard(), null, " "));
|
|
}
|
|
};
|
|
|
|
// create customize link
|
|
v.tools.createCustomizeLink = function() {
|
|
if (!v.status.customize &&
|
|
(v.conf.debug || document.querySelector("#apex-dev-toolbar") || document.querySelector("#apexDevToolbar"))
|
|
) {
|
|
if (document.querySelector("#" + v.dom.containerId + " svg text.link") === null) {
|
|
v.dom.svg.append("svg:text")
|
|
.attr("class", "link")
|
|
.attr("x", 5)
|
|
.attr("y", 15)
|
|
.attr("text-anchor", "start")
|
|
.text("Customize Me")
|
|
.on("click", function() {
|
|
graph.customize(true);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
// remove customize link
|
|
v.tools.removeCustomizeLink = function() {
|
|
v.dom.svg.select("#" + v.dom.containerId + " svg text.link").remove();
|
|
};
|
|
|
|
// dragability for customizing container
|
|
v.tools.customizeDrag = d3.behavior.drag()
|
|
.on("dragstart", function() {
|
|
var mouseToBody = d3.mouse(document.body);
|
|
v.dom.customizePosition = v.tools.getOffsetRect(document.querySelector("#" + v.dom.containerId +
|
|
"_customizing"));
|
|
v.dom.customizePosition.mouseLeft = mouseToBody[0] - v.dom.customizePosition.left;
|
|
v.dom.customizePosition.mouseTop = mouseToBody[1] - v.dom.customizePosition.top;
|
|
})
|
|
.on("drag", function() {
|
|
var mouseToBody = d3.mouse(document.body);
|
|
v.dom.customize
|
|
.style("left", Math.max(0,
|
|
mouseToBody[0] - v.dom.customizePosition.mouseLeft) + "px")
|
|
.style("top", Math.max(0,
|
|
mouseToBody[1] - v.dom.customizePosition.mouseTop) + "px");
|
|
})
|
|
.on("dragend", function() {
|
|
//v.dom.customizePosition = v.tools.getOffsetRect(document.querySelector("#" + v.dom.containerId +
|
|
//"_customizing"));
|
|
v.dom.customizePosition = v.tools.getOffsetRect(v.dom.customize.node());
|
|
});
|
|
|
|
// create customize wizard, if graph not rendering
|
|
v.tools.createCustomizeWizardIfNotRendering = function() {
|
|
if (v.status.customize && !v.status.graphRendering) {
|
|
v.tools.createCustomizeWizard();
|
|
}
|
|
};
|
|
|
|
// customize wizard
|
|
v.tools.createCustomizeWizard = function() {
|
|
/* jshint -W074, -W071 */
|
|
var grid, gridRow, gridCell, row, td, form, i = 4,
|
|
currentOption, valueInOptions, key;
|
|
var releaseFixedNodesAndResume = function() {
|
|
graph.releaseFixedNodes().resume();
|
|
};
|
|
var onSelectChange = function() {
|
|
v.status.customizeCurrentTabPosition = this.id;
|
|
if (v.confDefaults[this.name].type === "text") {
|
|
graph[this.name](this.options[this.selectedIndex].value).render();
|
|
} else if (v.confDefaults[this.name].type === "number") {
|
|
graph[this.name](parseFloat(this.options[this.selectedIndex].value)).render();
|
|
} else if (v.confDefaults[this.name].type === "bool") {
|
|
graph[this.name]((this.options[this.selectedIndex].value === "true")).render();
|
|
}
|
|
};
|
|
var appendOptionsToSelect = function(key) {
|
|
v.confDefaults[key].options.forEach(function(option) {
|
|
currentOption = option;
|
|
form.append("option")
|
|
.attr("value", option)
|
|
.attr("selected", function() {
|
|
if (v.confDefaults[key].type === "text" || v.confDefaults[key].type === "bool") {
|
|
if (currentOption === v.conf[key]) {
|
|
valueInOptions = true;
|
|
return "selected";
|
|
} else {
|
|
return null;
|
|
}
|
|
} else if (v.confDefaults[key].type === "number") {
|
|
if (parseFloat(currentOption) === v.conf[key]) {
|
|
valueInOptions = true;
|
|
return "selected";
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
})
|
|
.text(option);
|
|
});
|
|
};
|
|
// render customization wizard only if we have the right status, otherwise remove the wizard
|
|
if (!v.status.customize) {
|
|
v.tools.removeCustomizeWizard();
|
|
v.tools.createCustomizeLink();
|
|
} else {
|
|
v.tools.removeCustomizeLink();
|
|
// set initial position
|
|
if (!v.dom.customizePosition) {
|
|
v.dom.customizePosition = v.tools.getOffsetRect(v.dom.svg.node());
|
|
v.dom.customizePosition.left = v.dom.customizePosition.left + v.conf.width + 8;
|
|
}
|
|
if (document.querySelector("#" + v.dom.containerId + "_customizing") !== null) {
|
|
v.dom.customize.remove();
|
|
}
|
|
v.dom.customize = v.dom.body.insert("div")
|
|
.attr("id", v.dom.containerId + "_customizing")
|
|
.attr("class", "net_gobrechts_d3_force_customize")
|
|
.style("left", v.dom.customizePosition.left + "px")
|
|
.style("top", v.dom.customizePosition.top + "px");
|
|
v.dom.customize.append("span")
|
|
.attr("class", "drag")
|
|
.call(v.tools.customizeDrag)
|
|
.append("span")
|
|
.attr("class", "title")
|
|
.text("Customize \"" + v.dom.containerId + "\"");
|
|
v.dom.customize.append("a")
|
|
.attr("class", "close focus")
|
|
.attr("tabindex", 1)
|
|
.text("Close")
|
|
.on("click", function() {
|
|
v.status.customize = false;
|
|
v.tools.removeCustomizeWizard();
|
|
v.tools.createCustomizeLink();
|
|
})
|
|
.on("keydown", function() {
|
|
if (d3.event.keyCode === 13) {
|
|
v.status.customize = false;
|
|
v.tools.removeCustomizeWizard();
|
|
v.tools.createCustomizeLink();
|
|
}
|
|
});
|
|
grid = v.dom.customize.append("table");
|
|
gridRow = grid.append("tr");
|
|
gridCell = gridRow.append("td").style("vertical-align", "top");
|
|
v.dom.customizeMenu = gridCell.append("span");
|
|
v.dom.customizeOptionsTable = gridCell.append("table");
|
|
for (key in v.confDefaults) {
|
|
if (v.confDefaults.hasOwnProperty(key) && v.confDefaults[key].display) {
|
|
i += 1;
|
|
row = v.dom.customizeOptionsTable.append("tr")
|
|
.attr("class", v.confDefaults[key].relation + "-related");
|
|
row.append("td")
|
|
.attr("class", "label")
|
|
.html("<a href=\"https://gobrechts.net/wiki/projects/d3-force-apex-plugin#" +
|
|
key.toLowerCase() + "\" target=\"github_d3_force\" tabindex=\"" + i + 100 + "\">" +
|
|
key + "</a>");
|
|
td = row.append("td");
|
|
form = td.append("select")
|
|
.attr("id", v.dom.containerId + "_" + key)
|
|
.attr("name", key)
|
|
.attr("value", v.conf[key])
|
|
.attr("tabindex", i + 1)
|
|
.classed("warning", v.confDefaults[key].internal)
|
|
.on("change", onSelectChange);
|
|
valueInOptions = false;
|
|
appendOptionsToSelect(key);
|
|
// append current value if not existing in default options
|
|
if (!valueInOptions) {
|
|
form.append("option")
|
|
.attr("value", v.conf[key])
|
|
.attr("selected", "selected")
|
|
.text(v.conf[key]);
|
|
v.confDefaults[key].options.push(v.conf[key]);
|
|
}
|
|
// add short link to release all fixed (pinned) nodes
|
|
if (key === "pinMode") {
|
|
td.append("a")
|
|
.text("release all")
|
|
.attr("href", null)
|
|
.on("click", releaseFixedNodesAndResume);
|
|
}
|
|
}
|
|
}
|
|
v.dom.customizeOptionsTable.style("width", d3.select(v.dom.customizeOptionsTable).node()[0][0].clientWidth +
|
|
"px");
|
|
gridCell.append("span").html("<br>");
|
|
gridCell = gridRow.append("td")
|
|
.style("vertical-align", "top")
|
|
.style("padding-left", "5px");
|
|
gridCell.append("span")
|
|
.html("Your Configuration Object<p style=\"font-size:10px;margin:0;\">" +
|
|
(v.status.apexPluginId ?
|
|
"To save your options please copy<br>this to your plugin region attributes.<br>" +
|
|
"Only non-default options are shown.</p>" :
|
|
"Use this to initialize your graph.<br>Only non-default options are shown.</p>")
|
|
);
|
|
v.dom.customizeConfObject = gridCell.append("textarea")
|
|
.attr("tabindex", i + 5)
|
|
.attr("readonly", "readonly");
|
|
gridCell.append("span").html("<br><br>Current Positions<br>");
|
|
v.dom.customizePositions = gridCell.append("textarea")
|
|
.attr("tabindex", i + 6)
|
|
.attr("readonly", "readonly")
|
|
.text((v.status.forceRunning ? "Force started - wait for end event to show positions..." :
|
|
JSON.stringify(graph.positions())));
|
|
gridCell.append("span").html("<br><br>Debug Log (descending)<br>");
|
|
v.dom.customizeLog = gridCell.append("textarea")
|
|
.attr("tabindex", i + 7)
|
|
.attr("readonly", "readonly");
|
|
gridRow = grid.append("tr");
|
|
gridCell = gridRow.append("td")
|
|
.attr("colspan", 2)
|
|
.html("Copyrights:");
|
|
gridRow = grid.append("tr");
|
|
gridCell = gridRow.append("td")
|
|
.attr("colspan", 2)
|
|
.html("<table><tr><td style=\"padding-right:20px;\">" +
|
|
"<a href=\"https://github.com/ogobrecht/d3-force-apex-plugin\" target=\"_blank\" " +
|
|
"tabindex=\"" + (i + 8) + "\">D3 Force APEX Plugin</a> (" + v.version +
|
|
")<br>Ottmar Gobrecht</td><td style=\"padding-right:20px;\">" +
|
|
"<a href=\"https://github.com/mbostock/d3\" target=\"d3js_org\" tabindex=\"" + (i + 9) +
|
|
"\">D3.js</a> (" + d3.version + ") and " +
|
|
"<a href=\"https://github.com/d3/d3-plugins/tree/master/lasso\" target=\"_blank\" tabindex=\"" +
|
|
(i + 10) + "\">D3 Lasso Plugin</a> (modified)<br>Mike Bostock" +
|
|
"</td></tr><tr><td colspan=\"3\">" +
|
|
"<a href=\"https://github.com/tinker10/D3-Labeler\" target=\"github_d3_labeler\" " +
|
|
"tabindex=\"" + (i + 11) +
|
|
"\">D3 Labeler Plugin</a> (automatic label placement using simulated annealing)" +
|
|
"<br>Evan Wang</td></tr></table>"); // https://github.com/tinker10/D3-Labeler
|
|
v.tools.createCustomizeMenu(v.status.customizeCurrentMenu);
|
|
v.tools.writeConfObjectIntoWizard();
|
|
if (v.status.customizeCurrentTabPosition) {
|
|
document.getElementById(v.status.customizeCurrentTabPosition).focus();
|
|
}
|
|
}
|
|
};
|
|
|
|
v.tools.removeCustomizeWizard = function() {
|
|
d3.select("#" + v.dom.containerId + "_customizing").remove();
|
|
};
|
|
|
|
v.tools.createCustomizeMenu = function(relation) {
|
|
v.status.customizeCurrentMenu = relation;
|
|
v.dom.customizeMenu.selectAll("*").remove();
|
|
v.dom.customizeMenu.append("span").text("Show options for:");
|
|
if (v.status.customizeCurrentMenu === "nodes") {
|
|
v.dom.customizeMenu.append("span").style("font-weight", "bold").style("margin-left", "10px").text("NODES");
|
|
v.dom.customizeOptionsTable.selectAll("tr.node-related").classed("hidden", false);
|
|
v.dom.customizeOptionsTable.selectAll("tr.link-related,tr.graph-related").classed("hidden", true);
|
|
} else {
|
|
v.dom.customizeMenu.append("a")
|
|
.style("font-weight", "bold")
|
|
.style("margin-left", "10px")
|
|
.text("NODES")
|
|
.attr("tabindex", 2)
|
|
.on("click", function() {
|
|
v.tools.createCustomizeMenu("nodes");
|
|
v.dom.customizeOptionsTable.selectAll("tr.node-related").classed("hidden", false);
|
|
v.dom.customizeOptionsTable.selectAll("tr.link-related,tr.graph-related").classed("hidden", true);
|
|
})
|
|
.on("keydown", function() {
|
|
if (d3.event.keyCode === 13) {
|
|
v.tools.createCustomizeMenu("nodes");
|
|
v.dom.customizeOptionsTable.selectAll("tr.node-related").classed("hidden", false);
|
|
v.dom.customizeOptionsTable.selectAll("tr.link-related,tr.graph-related")
|
|
.classed("hidden", true);
|
|
}
|
|
});
|
|
}
|
|
if (v.status.customizeCurrentMenu === "links") {
|
|
v.dom.customizeMenu.append("span").style("font-weight", "bold").style("margin-left", "10px").text("LINKS");
|
|
v.dom.customizeOptionsTable.selectAll("tr.link-related").classed("hidden", false);
|
|
v.dom.customizeOptionsTable.selectAll("tr.node-related,tr.graph-related").classed("hidden", true);
|
|
} else {
|
|
v.dom.customizeMenu.append("a")
|
|
.style("font-weight", "bold")
|
|
.style("margin-left", "10px")
|
|
.text("LINKS")
|
|
.attr("tabindex", 3)
|
|
.on("click", function() {
|
|
v.tools.createCustomizeMenu("links");
|
|
v.dom.customizeOptionsTable.selectAll("tr.link-related").classed("hidden", false);
|
|
v.dom.customizeOptionsTable.selectAll("tr.node-related,tr.graph-related").classed("hidden", true);
|
|
})
|
|
.on("keydown", function() {
|
|
if (d3.event.keyCode === 13) {
|
|
v.tools.createCustomizeMenu("links");
|
|
v.dom.customizeOptionsTable.selectAll("tr.link-related").classed("hidden", false);
|
|
v.dom.customizeOptionsTable.selectAll("tr.node-related,tr.graph-related")
|
|
.classed("hidden", true);
|
|
}
|
|
});
|
|
}
|
|
if (v.status.customizeCurrentMenu === "graph") {
|
|
v.dom.customizeMenu.append("span").style("font-weight", "bold").style("margin-left", "10px").text("GRAPH");
|
|
v.dom.customizeOptionsTable.selectAll("tr.graph-related").classed("hidden", false);
|
|
v.dom.customizeOptionsTable.selectAll("tr.node-related,tr.link-related").classed("hidden", true);
|
|
} else {
|
|
v.dom.customizeMenu.append("a")
|
|
.style("font-weight", "bold")
|
|
.style("margin-left", "10px")
|
|
.text("GRAPH")
|
|
.attr("tabindex", 4)
|
|
.on("click", function() {
|
|
v.tools.createCustomizeMenu("graph");
|
|
v.dom.customizeOptionsTable.selectAll("tr.graph-related").classed("hidden", false);
|
|
v.dom.customizeOptionsTable.selectAll("tr.node-related,tr.link-related").classed("hidden", true);
|
|
})
|
|
.on("keydown", function() {
|
|
if (d3.event.keyCode === 13) {
|
|
v.tools.createCustomizeMenu("graph");
|
|
v.dom.customizeOptionsTable.selectAll("tr.graph-related").classed("hidden", false);
|
|
v.dom.customizeOptionsTable.selectAll("tr.node-related,tr.link-related")
|
|
.classed("hidden", true);
|
|
}
|
|
});
|
|
}
|
|
v.dom.customizeMenu.append("span").html("<br><br>");
|
|
};
|
|
|
|
/*******************************************************************************************************************
|
|
* LIBRARIES
|
|
*/
|
|
|
|
// D3 labeler plugin
|
|
/* Source Code: https://github.com/tinker10/D3-Labeler
|
|
The MIT License (MIT)
|
|
|
|
Copyright (c) 2013 Evan Wang
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
SOFTWARE.
|
|
*/
|
|
v.lib.labelerPlugin = function() {
|
|
/* jshint -W106 */
|
|
var lab = [],
|
|
anc = [],
|
|
w = 1, // box width
|
|
h = 1, // box width
|
|
labeler = {};
|
|
|
|
var max_move = 5, //5.0,
|
|
max_angle = 0.5, //0.5,
|
|
acc = 0,
|
|
rej = 0;
|
|
|
|
// weights
|
|
var w_len = 0.2, // leader line length
|
|
w_inter = 1.0, // leader line intersection
|
|
w_lab2 = 30.0, // label-label overlap
|
|
w_lab_anc = 30.0, // label-anchor overlap
|
|
w_orient = 1.0; //3.0; // orientation bias
|
|
|
|
// booleans for user defined functions
|
|
var user_energy = false,
|
|
user_schedule = false;
|
|
|
|
var user_defined_energy,
|
|
user_defined_schedule;
|
|
|
|
var energy = function(index) {
|
|
/* jshint -W071 */
|
|
// energy function, tailored for label placement
|
|
|
|
var m = lab.length,
|
|
ener = 0,
|
|
dx = lab[index].x - anc[index].x,
|
|
dy = anc[index].y - lab[index].y,
|
|
dist = Math.sqrt(dx * dx + dy * dy),
|
|
overlap = true;
|
|
|
|
// penalty for length of leader line
|
|
if (dist > 0) {
|
|
ener += dist * w_len;
|
|
}
|
|
|
|
// label orientation bias
|
|
dx /= dist;
|
|
dy /= dist;
|
|
if (dx > 0 && dy > 0) {
|
|
ener += 0;
|
|
} else if (dx < 0 && dy > 0) {
|
|
ener += w_orient;
|
|
} else if (dx < 0 && dy < 0) {
|
|
ener += 2 * w_orient;
|
|
} else {
|
|
ener += 3 * w_orient;
|
|
}
|
|
|
|
var x21 = lab[index].x,
|
|
y21 = lab[index].y - lab[index].height + 2.0,
|
|
x22 = lab[index].x + lab[index].width,
|
|
y22 = lab[index].y + 2.0;
|
|
var x11, x12, y11, y12, x_overlap, y_overlap, overlap_area;
|
|
|
|
for (var i = 0; i < m; i++) {
|
|
if (i !== index) {
|
|
|
|
// penalty for intersection of leader lines
|
|
overlap = intersect(anc[index].x, lab[index].x, anc[i].x, lab[i].x,
|
|
anc[index].y, lab[index].y, anc[i].y, lab[i].y);
|
|
if (overlap) {
|
|
ener += w_inter;
|
|
}
|
|
|
|
// penalty for label-label overlap
|
|
x11 = lab[i].x;
|
|
y11 = lab[i].y - lab[i].height + 2.0;
|
|
x12 = lab[i].x + lab[i].width;
|
|
y12 = lab[i].y + 2.0;
|
|
x_overlap = Math.max(0, Math.min(x12, x22) - Math.max(x11, x21));
|
|
y_overlap = Math.max(0, Math.min(y12, y22) - Math.max(y11, y21));
|
|
overlap_area = x_overlap * y_overlap;
|
|
ener += (overlap_area * w_lab2);
|
|
}
|
|
|
|
// penalty for label-anchor overlap
|
|
x11 = anc[i].x - anc[i].r;
|
|
y11 = anc[i].y - anc[i].r;
|
|
x12 = anc[i].x + anc[i].r;
|
|
y12 = anc[i].y + anc[i].r;
|
|
x_overlap = Math.max(0, Math.min(x12, x22) - Math.max(x11, x21));
|
|
y_overlap = Math.max(0, Math.min(y12, y22) - Math.max(y11, y21));
|
|
overlap_area = x_overlap * y_overlap;
|
|
ener += (overlap_area * w_lab_anc);
|
|
|
|
}
|
|
return ener;
|
|
};
|
|
|
|
var mcmove = function(currT) {
|
|
// Monte Carlo translation move
|
|
|
|
// select a random label
|
|
var i = Math.floor(Math.random() * lab.length);
|
|
|
|
// save old coordinates
|
|
var x_old = lab[i].x;
|
|
var y_old = lab[i].y;
|
|
|
|
// old energy
|
|
var old_energy;
|
|
if (user_energy) {
|
|
old_energy = user_defined_energy(i, lab, anc);
|
|
} else {
|
|
old_energy = energy(i);
|
|
}
|
|
|
|
// random translation
|
|
lab[i].x += (Math.random() - 0.5) * max_move;
|
|
lab[i].y += (Math.random() - 0.5) * max_move;
|
|
|
|
// hard wall boundaries
|
|
if (lab[i].x > w) {
|
|
lab[i].x = x_old;
|
|
}
|
|
if (lab[i].x < 0) {
|
|
lab[i].x = x_old;
|
|
}
|
|
if (lab[i].y > h) {
|
|
lab[i].y = y_old;
|
|
}
|
|
if (lab[i].y < 0) {
|
|
lab[i].y = y_old;
|
|
}
|
|
|
|
// new energy
|
|
var new_energy;
|
|
if (user_energy) {
|
|
new_energy = user_defined_energy(i, lab, anc);
|
|
} else {
|
|
new_energy = energy(i);
|
|
}
|
|
|
|
// delta E
|
|
var delta_energy = new_energy - old_energy;
|
|
|
|
if (Math.random() < Math.exp(-delta_energy / currT)) {
|
|
acc += 1;
|
|
} else {
|
|
// move back to old coordinates
|
|
lab[i].x = x_old;
|
|
lab[i].y = y_old;
|
|
rej += 1;
|
|
}
|
|
|
|
};
|
|
|
|
var mcrotate = function(currT) {
|
|
/* jshint -W071 */
|
|
// Monte Carlo rotation move
|
|
|
|
// select a random label
|
|
var i = Math.floor(Math.random() * lab.length);
|
|
|
|
// save old coordinates
|
|
var x_old = lab[i].x;
|
|
var y_old = lab[i].y;
|
|
|
|
// old energy
|
|
var old_energy;
|
|
if (user_energy) {
|
|
old_energy = user_defined_energy(i, lab, anc);
|
|
} else {
|
|
old_energy = energy(i);
|
|
}
|
|
|
|
// random angle
|
|
var angle = (Math.random() - 0.5) * max_angle;
|
|
|
|
var s = Math.sin(angle);
|
|
var c = Math.cos(angle);
|
|
|
|
// translate label (relative to anchor at origin):
|
|
lab[i].x -= anc[i].x;
|
|
lab[i].y -= anc[i].y;
|
|
|
|
// rotate label
|
|
var x_new = lab[i].x * c - lab[i].y * s,
|
|
y_new = lab[i].x * s + lab[i].y * c;
|
|
|
|
// translate label back
|
|
lab[i].x = x_new + anc[i].x;
|
|
lab[i].y = y_new + anc[i].y;
|
|
|
|
// hard wall boundaries
|
|
if (lab[i].x > w) {
|
|
lab[i].x = x_old;
|
|
}
|
|
if (lab[i].x < 0) {
|
|
lab[i].x = x_old;
|
|
}
|
|
if (lab[i].y > h) {
|
|
lab[i].y = y_old;
|
|
}
|
|
if (lab[i].y < 0) {
|
|
lab[i].y = y_old;
|
|
}
|
|
|
|
// new energy
|
|
var new_energy;
|
|
if (user_energy) {
|
|
new_energy = user_defined_energy(i, lab, anc);
|
|
} else {
|
|
new_energy = energy(i);
|
|
}
|
|
|
|
// delta E
|
|
var delta_energy = new_energy - old_energy;
|
|
|
|
if (Math.random() < Math.exp(-delta_energy / currT)) {
|
|
acc += 1;
|
|
} else {
|
|
// move back to old coordinates
|
|
lab[i].x = x_old;
|
|
lab[i].y = y_old;
|
|
rej += 1;
|
|
}
|
|
|
|
};
|
|
|
|
var intersect = function(x1, x2, x3, x4, y1, y2, y3, y4) {
|
|
// returns true if two lines intersect, else false
|
|
// from http://paulbourke.net/geometry/lineline2d/
|
|
|
|
var mua, mub;
|
|
var denom, numera, numerb;
|
|
|
|
denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1);
|
|
numera = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3);
|
|
numerb = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3);
|
|
|
|
/* Is the intersection along the the segments */
|
|
mua = numera / denom;
|
|
mub = numerb / denom;
|
|
return !(mua < 0 || mua > 1 || mub < 0 || mub > 1);
|
|
|
|
};
|
|
|
|
var cooling_schedule = function(currT, initialT, nsweeps) {
|
|
// linear cooling
|
|
return (currT - (initialT / nsweeps));
|
|
};
|
|
|
|
labeler.start = function(nsweeps) {
|
|
// main simulated annealing function
|
|
var m = lab.length,
|
|
currT = 1.0,
|
|
initialT = 1.0;
|
|
|
|
for (var i = 0; i < nsweeps; i++) {
|
|
for (var j = 0; j < m; j++) {
|
|
if (Math.random() < 0.5) {
|
|
mcmove(currT);
|
|
} else {
|
|
mcrotate(currT);
|
|
}
|
|
}
|
|
currT = cooling_schedule(currT, initialT, nsweeps);
|
|
}
|
|
};
|
|
|
|
labeler.width = function(x) {
|
|
// users insert graph width
|
|
if (!arguments.length) {
|
|
return w;
|
|
}
|
|
w = x;
|
|
return labeler;
|
|
};
|
|
|
|
labeler.height = function(x) {
|
|
// users insert graph height
|
|
if (!arguments.length) {
|
|
return h;
|
|
}
|
|
h = x;
|
|
return labeler;
|
|
};
|
|
|
|
labeler.label = function(x) {
|
|
// users insert label positions
|
|
if (!arguments.length) {
|
|
return lab;
|
|
}
|
|
lab = x;
|
|
return labeler;
|
|
};
|
|
|
|
labeler.anchor = function(x) {
|
|
// users insert anchor positions
|
|
if (!arguments.length) {
|
|
return anc;
|
|
}
|
|
anc = x;
|
|
return labeler;
|
|
};
|
|
|
|
labeler.alt_energy = function(x) {
|
|
// user defined energy
|
|
if (!arguments.length) {
|
|
return energy;
|
|
}
|
|
user_defined_energy = x;
|
|
user_energy = true;
|
|
return labeler;
|
|
};
|
|
|
|
labeler.alt_schedule = function(x) {
|
|
// user defined cooling_schedule
|
|
if (!arguments.length) {
|
|
return cooling_schedule;
|
|
}
|
|
user_defined_schedule = x;
|
|
user_schedule = true;
|
|
return labeler;
|
|
};
|
|
|
|
return labeler;
|
|
};
|
|
|
|
// D3 lasso plugin
|
|
/* Source Code: https://github.com/d3/d3-plugins/blob/master/lasso/lasso.js
|
|
Copyright (c) 2012-2014, Michael Bostock
|
|
All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
modification, are permitted provided that the following conditions are met:
|
|
|
|
* Redistributions of source code must retain the above copyright notice, this
|
|
list of conditions and the following disclaimer.
|
|
|
|
* Redistributions in binary form must reproduce the above copyright notice,
|
|
this list of conditions and the following disclaimer in the documentation
|
|
and/or other materials provided with the distribution.
|
|
|
|
* The name Michael Bostock may not be used to endorse or promote products
|
|
derived from this software without specific prior written permission.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
DISCLAIMED. IN NO EVENT SHALL MICHAEL BOSTOCK BE LIABLE FOR ANY DIRECT,
|
|
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
|
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
v.lib.lassoPlugin = function() {
|
|
/* jshint -W040, -W106 */
|
|
var items = null,
|
|
closePathDistance = 75,
|
|
closePathSelect = true,
|
|
isPathClosed = false,
|
|
hoverSelect = true,
|
|
area = null,
|
|
pathContainer = null,
|
|
on = {
|
|
start: function() {},
|
|
draw: function() {},
|
|
end: function() {}
|
|
};
|
|
|
|
function lasso() {
|
|
var _this = d3.select(this[0][0]);
|
|
/* START MODIFICATION ------------------------------------------------------>
|
|
* Reuse lasso path group element, if possible. In my D3 force implementation
|
|
* I provide the possibility to enable or disable the lasso. After enabling
|
|
* the lasso I get always a new lasso element. I prefer to reuse the existing
|
|
* one.
|
|
* */
|
|
//
|
|
var g, dyn_path, close_path, complete_path, path, origin, last_known_point, path_length_start, drag;
|
|
pathContainer = pathContainer || _this; // if not set then defaults to _this
|
|
if (pathContainer.selectAll("g.lasso").size() === 0) {
|
|
g = pathContainer.append("g").attr("class", "lasso");
|
|
dyn_path = g.append("path").attr("class", "drawn");
|
|
close_path = g.append("path").attr("class", "loop_close");
|
|
complete_path = g.append("path").attr("class", "complete_path").attr("display", "none");
|
|
} else {
|
|
g = pathContainer.select("g.lasso");
|
|
dyn_path = g.select("path.drawn");
|
|
close_path = g.select("path.loop_close");
|
|
complete_path = g.select("path.complete_path");
|
|
}
|
|
/* <-------------------------------------------------------- END MODIFICATION */
|
|
|
|
function dragstart() {
|
|
// Reset blank lasso path
|
|
path = "";
|
|
dyn_path.attr("d", null);
|
|
close_path.attr("d", null);
|
|
// Set path length start
|
|
path_length_start = 0;
|
|
// Set every item to have a false selection and reset their center point and counters
|
|
items[0].forEach(function(d) {
|
|
d.hoverSelected = false;
|
|
d.loopSelected = false;
|
|
var cur_box = d.getBBox();
|
|
/* START MODIFICATION ------------------------------------------------------>
|
|
* Implement correct values after zoom and pan based on the following article:
|
|
* http://stackoverflow.com/questions/18554224/getting-screen-positions-of-d3-nodes-after-transform
|
|
* */
|
|
var ctm = d.getCTM();
|
|
d.lassoPoint = {
|
|
cx: Math.round((cur_box.x + cur_box.width / 2) * ctm.a + ctm.e),
|
|
cy: Math.round((cur_box.y + cur_box.height / 2) * ctm.d + ctm.f),
|
|
/* <-------------------------------------------------------- END MODIFICATION */
|
|
edges: {
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
left: 0
|
|
},
|
|
close_edges: {
|
|
left: 0,
|
|
right: 0
|
|
}
|
|
};
|
|
});
|
|
|
|
// if hover is on, add hover function
|
|
if (hoverSelect === true) {
|
|
items.on("mouseover.lasso", function() {
|
|
// if hovered, change lasso selection attribute to true
|
|
d3.select(this)[0][0].hoverSelected = true;
|
|
});
|
|
}
|
|
|
|
// Run user defined start function
|
|
on.start();
|
|
}
|
|
|
|
function dragmove() {
|
|
/* jshint -W071 */
|
|
var x = d3.mouse(this)[0],
|
|
y = d3.mouse(this)[1],
|
|
distance,
|
|
close_draw_path,
|
|
complete_path_d,
|
|
close_path_node,
|
|
close_path_length,
|
|
close_path_edges,
|
|
path_node,
|
|
path_length_end,
|
|
i,
|
|
last_pos,
|
|
prior_pos,
|
|
prior_pos_obj,
|
|
cur_pos,
|
|
cur_pos_obj,
|
|
calcLassoPointEdges = function(d) {
|
|
if (cur_pos_obj.x > d.lassoPoint.cx) {
|
|
d.lassoPoint.edges.right = d.lassoPoint.edges.right + 1;
|
|
}
|
|
if (cur_pos_obj.x < d.lassoPoint.cx) {
|
|
d.lassoPoint.edges.left = d.lassoPoint.edges.left + 1;
|
|
}
|
|
},
|
|
calcLassoPointCloseEdges = function(d) {
|
|
if (Math.round(cur_pos.y) !== Math.round(prior_pos.y) &&
|
|
Math.round(cur_pos.x) > d.lassoPoint.cx) {
|
|
d.lassoPoint.close_edges.right = 1;
|
|
}
|
|
if (Math.round(cur_pos.y) !== Math.round(prior_pos.y) &&
|
|
Math.round(cur_pos.x) < d.lassoPoint.cx) {
|
|
d.lassoPoint.close_edges.left = 1;
|
|
}
|
|
},
|
|
ckeckIfNodeYequalsCurrentPosY = function(d) {
|
|
return d.lassoPoint.cy === Math.round(cur_pos.y);
|
|
},
|
|
ckeckIfNodeYequalsCurrentPriorPosY = function(d) {
|
|
var a;
|
|
if (d.lassoPoint.cy === cur_pos_obj.y && d.lassoPoint.cy !== prior_pos_obj.y) {
|
|
last_known_point = {
|
|
x: prior_pos_obj.x,
|
|
y: prior_pos_obj.y
|
|
};
|
|
a = false;
|
|
} else if (d.lassoPoint.cy === cur_pos_obj.y && d.lassoPoint.cy === prior_pos_obj.y) {
|
|
a = false;
|
|
} else if (d.lassoPoint.cy === prior_pos_obj.y && d.lassoPoint.cy !== cur_pos_obj.y) {
|
|
a = sign(d.lassoPoint.cy - cur_pos_obj.y) !== sign(d.lassoPoint.cy - last_known_point.y);
|
|
} else {
|
|
last_known_point = {
|
|
x: prior_pos_obj.x,
|
|
y: prior_pos_obj.y
|
|
};
|
|
a = sign(d.lassoPoint.cy - cur_pos_obj.y) !== sign(d.lassoPoint.cy - prior_pos_obj.y);
|
|
}
|
|
return a;
|
|
};
|
|
|
|
// Initialize the path or add the latest point to it
|
|
if (path === "") {
|
|
path = path + "M " + x + " " + y;
|
|
origin = [x, y];
|
|
} else {
|
|
path = path + " L " + x + " " + y;
|
|
}
|
|
|
|
// Reset closed edges counter
|
|
items[0].forEach(function(d) {
|
|
d.lassoPoint.close_edges = {
|
|
left: 0,
|
|
right: 0
|
|
};
|
|
});
|
|
|
|
// Calculate the current distance from the lasso origin
|
|
distance = Math.sqrt(Math.pow(x - origin[0], 2) + Math.pow(y - origin[1], 2));
|
|
|
|
// Set the closed path line
|
|
close_draw_path = "M " + x + " " + y + " L " + origin[0] + " " + origin[1];
|
|
|
|
// Draw the lines
|
|
dyn_path.attr("d", path);
|
|
|
|
// If within the closed path distance parameter, show the closed path. otherwise, hide it
|
|
if (distance <= closePathDistance) {
|
|
close_path.attr("display", null);
|
|
} else {
|
|
close_path.attr("display", "none");
|
|
}
|
|
|
|
isPathClosed = distance <= closePathDistance;
|
|
|
|
// create complete path
|
|
complete_path_d = d3.select("path")[0][0].attributes.d.value + "Z";
|
|
complete_path.attr("d", complete_path_d);
|
|
|
|
// get path length
|
|
path_node = dyn_path.node();
|
|
path_length_end = path_node.getTotalLength();
|
|
last_pos = path_node.getPointAtLength(path_length_start - 1);
|
|
|
|
for (i = path_length_start; i <= path_length_end; i++) {
|
|
cur_pos = path_node.getPointAtLength(i);
|
|
cur_pos_obj = {
|
|
x: Math.round(cur_pos.x * 100) / 100,
|
|
y: Math.round(cur_pos.y * 100) / 100
|
|
};
|
|
prior_pos = path_node.getPointAtLength(i - 1);
|
|
prior_pos_obj = {
|
|
x: Math.round(prior_pos.x * 100) / 100,
|
|
y: Math.round(prior_pos.y * 100) / 100
|
|
};
|
|
|
|
items[0].filter(ckeckIfNodeYequalsCurrentPriorPosY).forEach(calcLassoPointEdges);
|
|
}
|
|
|
|
if (isPathClosed === true && closePathSelect === true) {
|
|
close_path.attr("d", close_draw_path);
|
|
close_path_node = close_path.node();
|
|
close_path_length = close_path_node.getTotalLength();
|
|
close_path_edges = {
|
|
left: 0,
|
|
right: 0
|
|
};
|
|
for (i = 0; i <= close_path_length; i++) {
|
|
cur_pos = close_path_node.getPointAtLength(i);
|
|
prior_pos = close_path_node.getPointAtLength(i - 1);
|
|
items[0].filter(ckeckIfNodeYequalsCurrentPosY).forEach(calcLassoPointCloseEdges);
|
|
}
|
|
items[0].forEach(function(a) {
|
|
if ((a.lassoPoint.edges.left + a.lassoPoint.close_edges.left) > 0 &&
|
|
(a.lassoPoint.edges.right + a.lassoPoint.close_edges.right) % 2 === 1) {
|
|
a.loopSelected = true;
|
|
} else {
|
|
a.loopSelected = false;
|
|
}
|
|
});
|
|
} else {
|
|
items[0].forEach(function(d) {
|
|
d.loopSelected = false;
|
|
});
|
|
}
|
|
|
|
// Tag possible items
|
|
d3.selectAll(items[0].filter(function(d) {
|
|
return (d.loopSelected && isPathClosed) || d.hoverSelected;
|
|
}))
|
|
.attr("d", function(d) {
|
|
d.possible = true;
|
|
return d.possible;
|
|
});
|
|
|
|
d3.selectAll(items[0].filter(function(d) {
|
|
return !((d.loopSelected && isPathClosed) || d.hoverSelected);
|
|
}))
|
|
.attr("d", function(d) {
|
|
d.possible = false;
|
|
return d.possible;
|
|
});
|
|
|
|
on.draw();
|
|
|
|
// Continue drawing path from where it left off
|
|
path_length_start = path_length_end + 1;
|
|
}
|
|
|
|
function dragend() {
|
|
// Remove mouseover tagging function
|
|
items.on("mouseover.lasso", null);
|
|
|
|
// Tag selected items
|
|
items.filter(function(d) {
|
|
return d.possible === true;
|
|
})
|
|
.attr("d", function(d) {
|
|
d.selected = true;
|
|
return d.selected;
|
|
});
|
|
|
|
items.filter(function(d) {
|
|
return d.possible === false;
|
|
})
|
|
.attr("d", function(d) {
|
|
d.selected = false;
|
|
return d.selected;
|
|
});
|
|
|
|
// Reset possible items
|
|
items.attr("d", function(d) {
|
|
d.possible = false;
|
|
return d.possible;
|
|
});
|
|
|
|
// Clear lasso
|
|
dyn_path.attr("d", null);
|
|
close_path.attr("d", null);
|
|
|
|
// Run user defined end function
|
|
on.end();
|
|
|
|
}
|
|
drag = d3.behavior.drag()
|
|
.on("dragstart", dragstart)
|
|
.on("drag", dragmove)
|
|
.on("dragend", dragend);
|
|
area.call(drag);
|
|
}
|
|
|
|
lasso.items = function(_) {
|
|
|
|
if (!arguments.length) {
|
|
return items;
|
|
}
|
|
items = _;
|
|
items[0].forEach(function(d) {
|
|
var item = d3.select(d);
|
|
if (typeof item.datum() === "undefined") {
|
|
item.datum({
|
|
possible: false,
|
|
selected: false
|
|
});
|
|
} else {
|
|
item.attr("d", function(e) {
|
|
e.possible = false;
|
|
e.selected = false;
|
|
return e;
|
|
});
|
|
}
|
|
});
|
|
return lasso;
|
|
};
|
|
|
|
lasso.closePathDistance = function(_) {
|
|
if (!arguments.length) {
|
|
return closePathDistance;
|
|
}
|
|
closePathDistance = _;
|
|
return lasso;
|
|
};
|
|
|
|
lasso.closePathSelect = function(_) {
|
|
if (!arguments.length) {
|
|
return closePathSelect;
|
|
}
|
|
closePathSelect = _ === true;
|
|
return lasso;
|
|
};
|
|
|
|
lasso.isPathClosed = function(_) {
|
|
if (!arguments.length) {
|
|
return isPathClosed;
|
|
}
|
|
isPathClosed = _ === true;
|
|
return lasso;
|
|
};
|
|
|
|
lasso.hoverSelect = function(_) {
|
|
if (!arguments.length) {
|
|
return hoverSelect;
|
|
}
|
|
hoverSelect = _ === true;
|
|
return lasso;
|
|
};
|
|
|
|
lasso.on = function(type, _) {
|
|
if (!arguments.length) {
|
|
return on;
|
|
}
|
|
if (arguments.length === 1) {
|
|
return on[type];
|
|
}
|
|
var types = ["start", "draw", "end"];
|
|
if (types.indexOf(type) > -1) {
|
|
on[type] = _;
|
|
}
|
|
return lasso;
|
|
};
|
|
|
|
lasso.area = function(_) {
|
|
if (!arguments.length) {
|
|
return area;
|
|
}
|
|
area = _;
|
|
return lasso;
|
|
};
|
|
|
|
/* START MODIFICATION ------------------------------------------------------>
|
|
* Allow different container for lasso path than area, where lasso can be started
|
|
* */
|
|
lasso.pathContainer = function(_) {
|
|
if (!arguments.length) {
|
|
return pathContainer;
|
|
}
|
|
pathContainer = d3.select(_[0][0]);
|
|
return lasso;
|
|
};
|
|
/* <-------------------------------------------------------- END MODIFICATION */
|
|
|
|
function sign(x) {
|
|
return x ? x < 0 ? -1 : 1 : 0;
|
|
}
|
|
|
|
return lasso;
|
|
};
|
|
|
|
|
|
/*******************************************************************************************************************
|
|
* MAIN
|
|
*/
|
|
|
|
v.main.init();
|
|
|
|
|
|
/*******************************************************************************************************************
|
|
* PUBLIC GRAPH FUNCTION AND API METHODS
|
|
*/
|
|
|
|
function graph() {}
|
|
|
|
// public start function: get data and start visualization
|
|
graph.start = function(pData) {
|
|
var firstChar;
|
|
// try to use the input data - this means also, we can overwrite the data from APEX with raw data (textarea or
|
|
// whatever you like...)
|
|
if (pData) {
|
|
graph.render(pData);
|
|
}
|
|
// if we have no data, then we try to use the APEX context (if APEX plugin ID is set)
|
|
else if (v.status.apexPluginId) {
|
|
if (v.conf.showLoadingIndicatorOnAjaxCall) {
|
|
graph.showLoadingIndicator(true);
|
|
}
|
|
apex.server.plugin(
|
|
v.status.apexPluginId, {
|
|
p_debug: $v("pdebug"), //jshint ignore:line
|
|
pageItems: v.status.apexPageItemsToSubmit
|
|
}, {
|
|
success: function(dataString) {
|
|
// dataString starts NOT with "<" or "{", when there are no queries defined in APEX or
|
|
// when the queries returns empty data or when a error occurs on the APEX backend side
|
|
if (v.conf.showLoadingIndicatorOnAjaxCall) {
|
|
graph.showLoadingIndicator(false);
|
|
}
|
|
firstChar = dataString.trim().substr(0, 1);
|
|
if (firstChar === "<" || firstChar === "{") {
|
|
graph.render(dataString);
|
|
} else if (dataString.trim().substr(0, 16) === "no_query_defined") {
|
|
// this will keep the old data or using the sample data, if no old data existing
|
|
graph.render();
|
|
v.tools.logError("No query defined.");
|
|
} else if (dataString.trim().substr(0, 22) === "query_returned_no_data") {
|
|
graph.render({
|
|
"data": {
|
|
"nodes": [{
|
|
"ID": "1",
|
|
"LABEL": "ERROR: No data.",
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}],
|
|
"links": []
|
|
}
|
|
});
|
|
v.tools.logError("Query returned no data.");
|
|
} else {
|
|
graph.render({
|
|
"data": {
|
|
"nodes": [{
|
|
"ID": "1",
|
|
"LABEL": "ERROR: " + dataString + ".",
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}],
|
|
"links": []
|
|
}
|
|
});
|
|
v.tools.logError(dataString);
|
|
}
|
|
},
|
|
error: function(xhr, status, errorThrown) {
|
|
graph.render({
|
|
"data": {
|
|
"nodes": [{
|
|
"ID": "1",
|
|
"LABEL": "AJAX call terminated with errors.",
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}],
|
|
"links": []
|
|
}
|
|
});
|
|
v.tools.logError("AJAX call terminated with errors: " + errorThrown + ".");
|
|
},
|
|
dataType: "text"
|
|
}
|
|
);
|
|
}
|
|
// if we have no raw data and no APEX context, then we start to render without data (the render function will
|
|
// then provide sample data)
|
|
else {
|
|
graph.render();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.render = function(pData) {
|
|
/* jshint -W074, -W071 */
|
|
var message;
|
|
v.status.graphStarted = true;
|
|
v.status.graphRendering = true;
|
|
|
|
v.tools.triggerApexEvent(document.querySelector("#" + v.dom.containerId), "apexbeforerefresh");
|
|
|
|
// if we start the rendering the first time and there is no input data, then provide sample data
|
|
if (!pData && !v.status.graphReady) {
|
|
v.tools.logError("Houston, we have a problem - we have to provide sample data.");
|
|
v.status.sampleData = true;
|
|
pData = v.data.sampleData;
|
|
} else if (pData) {
|
|
v.status.sampleData = false;
|
|
}
|
|
|
|
// if we have incoming data, than we do our transformations here, otherwise we use the existing data
|
|
if (pData) {
|
|
|
|
if (v.status.graphReady) {
|
|
v.status.graphOldPositions = graph.positions();
|
|
}
|
|
|
|
// data is an object
|
|
if (pData.constructor === Object) {
|
|
v.data.dataConverted = pData;
|
|
if (v.conf.debug) {
|
|
v.tools.log("Data object:");
|
|
v.tools.log(v.data.dataConverted, true);
|
|
}
|
|
}
|
|
// data is a string
|
|
else if (pData.constructor === String) {
|
|
// convert incoming data depending on type
|
|
if (pData.trim().substr(0, 1) === "<") {
|
|
try {
|
|
v.data.dataConverted = v.tools.xmlToJson(v.tools.parseXml(pData));
|
|
if (v.data.dataConverted === null) {
|
|
message = "Unable to convert XML string.";
|
|
v.tools.logError(message);
|
|
v.data.dataConverted = {
|
|
"data": {
|
|
"nodes": [{
|
|
"ID": "1",
|
|
"LABEL": "ERROR: " + message,
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}],
|
|
"links": []
|
|
}
|
|
};
|
|
}
|
|
} catch (e) {
|
|
message = "Unable to convert XML string: " + e.message + ".";
|
|
v.tools.logError(message);
|
|
v.data.dataConverted = {
|
|
"data": {
|
|
"nodes": [{
|
|
"ID": "1",
|
|
"LABEL": "ERROR: " + message,
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}],
|
|
"links": []
|
|
}
|
|
};
|
|
}
|
|
} else if (pData.trim().substr(0, 1) === "{") {
|
|
try {
|
|
v.data.dataConverted = JSON.parse(pData);
|
|
} catch (e) {
|
|
message = "Unable to parse JSON string: " + e.message + ".";
|
|
v.tools.logError(message);
|
|
v.data.dataConverted = {
|
|
"data": {
|
|
"nodes": [{
|
|
"ID": "1",
|
|
"LABEL": "ERROR: " + message,
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}],
|
|
"links": []
|
|
}
|
|
};
|
|
}
|
|
} else {
|
|
message = "Your data string is not starting with \"<\" or \"{\" - parsing not possible.";
|
|
v.tools.logError(message);
|
|
v.data.dataConverted = {
|
|
"data": {
|
|
"nodes": [{
|
|
"ID": "1",
|
|
"LABEL": "ERROR: " + message,
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}],
|
|
"links": []
|
|
}
|
|
};
|
|
}
|
|
if (v.conf.debug) {
|
|
v.tools.log("Data string:");
|
|
v.tools.log(pData, true);
|
|
v.tools.log("Converted data object:");
|
|
v.tools.log(v.data.dataConverted, true);
|
|
}
|
|
}
|
|
// data has unknown format
|
|
else {
|
|
message = "Unable to parse your data - input data can be a XML string, " +
|
|
"JSON string or JavaScript object.";
|
|
v.tools.logError(message);
|
|
v.data.dataConverted = {
|
|
"data": {
|
|
"nodes": [{
|
|
"ID": "1",
|
|
"LABEL": "ERROR: " + message,
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}],
|
|
"links": []
|
|
}
|
|
};
|
|
}
|
|
|
|
// create references to our new data
|
|
if (v.data.dataConverted !== null) {
|
|
if (v.data.dataConverted.hasOwnProperty("data") && v.data.dataConverted.data !== null) {
|
|
if (v.data.dataConverted.data.hasOwnProperty("nodes") && v.data.dataConverted.data.nodes !== null) {
|
|
v.data.nodes = v.data.dataConverted.data.nodes;
|
|
if (v.data.nodes.length === 0) {
|
|
message = "Your data contains an empty nodes array.";
|
|
v.tools.logError(message);
|
|
v.data.nodes = [{
|
|
"ID": "1",
|
|
"LABEL": "ERROR: " + message,
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}];
|
|
}
|
|
} else {
|
|
message = "Your data contains no nodes.";
|
|
v.tools.logError(message);
|
|
v.data.nodes = [{
|
|
"ID": "1",
|
|
"LABEL": "ERROR: " + message,
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}];
|
|
}
|
|
if (v.data.dataConverted.data.hasOwnProperty("links") && v.data.dataConverted.data.links !== null) {
|
|
v.data.links = v.data.dataConverted.data.links;
|
|
} else {
|
|
v.data.links = [];
|
|
}
|
|
} else {
|
|
message = "Missing root element named data.";
|
|
v.tools.logError(message);
|
|
v.data = {
|
|
"nodes": [{
|
|
"ID": "1",
|
|
"LABEL": "ERROR: " + message,
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}],
|
|
"links": []
|
|
};
|
|
}
|
|
} else {
|
|
message = "Unable to parse your data - please consult the API reference for possible data formats.";
|
|
v.tools.logError(message);
|
|
v.data = {
|
|
"nodes": [{
|
|
"ID": "1",
|
|
"LABEL": "ERROR: " + message,
|
|
"COLORVALUE": "1",
|
|
"SIZEVALUE": "1"
|
|
}],
|
|
"links": []
|
|
};
|
|
}
|
|
|
|
// switch links to point to node objects instead of id's (needed for force layout) and calculate attributes
|
|
v.data.idLookup = []; // helper array to lookup node objects by id's
|
|
v.data.nodes.forEach(function(n) {
|
|
n.SIZEVALUE = parseFloat(n.SIZEVALUE); // convert size to float value
|
|
n.LABELCIRCULAR = v.tools.parseBool(n.LABELCIRCULAR); // convert labelCircular to boolean
|
|
if (n.fixed) {
|
|
n.fixed = v.tools.parseBool(n.fixed);
|
|
} // convert fixed to boolean
|
|
if (n.x) {
|
|
n.x = parseFloat(n.x);
|
|
} // convert X position to float value
|
|
if (n.y) {
|
|
n.y = parseFloat(n.y);
|
|
} // convert Y position to float value
|
|
v.data.idLookup[n.ID] = n; // add object reference to lookup array
|
|
});
|
|
v.data.links.forEach(function(l) {
|
|
l.source = v.data.idLookup[l.FROMID]; // add attribute source as a node reference to the link
|
|
l.target = v.data.idLookup[l.TOID]; // add attribute target as a node reference to the link
|
|
});
|
|
|
|
// sort out links with invalid node references
|
|
v.data.links = v.data.links.filter(function(l) {
|
|
return typeof l.source !== "undefined" && typeof l.target !== "undefined";
|
|
});
|
|
|
|
// create helper array to lookup if nodes are neighbors
|
|
v.data.neighbors = v.data.links.map(function(l) {
|
|
return l.FROMID + ":" + l.TOID;
|
|
});
|
|
|
|
// calculate distinct node colors for the legend
|
|
v.data.distinctNodeColorValues = v.data.nodes
|
|
.map(function(n) {
|
|
return (n.COLORLABEL ? n.COLORLABEL : "") + ";" + n.COLORVALUE;
|
|
})
|
|
// http://stackoverflow.com/questions/1960473/unique-values-in-an-array
|
|
.filter(function(value, index, self) {
|
|
return self.indexOf(value) === index;
|
|
})
|
|
.sort(function(a, b) { // http://www.sitepoint.com/sophisticated-sorting-in-javascript/
|
|
var x = a.toLowerCase(),
|
|
y = b.toLowerCase();
|
|
return x < y ? 1 : x > y ? -1 : 0;
|
|
});
|
|
|
|
// calculate distinct link colors for the markers
|
|
v.data.distinctLinkColorValues = v.data.links
|
|
.map(function(l) {
|
|
return l.COLOR;
|
|
})
|
|
// http://stackoverflow.com/questions/28607451/removing-undefined-values-from-array
|
|
// http://stackoverflow.com/questions/1960473/unique-values-in-an-array
|
|
.filter(Boolean)
|
|
.filter(function(value, index, self) {
|
|
return self.indexOf(value) === index;
|
|
})
|
|
.sort(function(a, b) { // http://www.sitepoint.com/sophisticated-sorting-in-javascript/
|
|
var x = a.toLowerCase(),
|
|
y = b.toLowerCase();
|
|
return x < y ? 1 : x > y ? -1 : 0;
|
|
});
|
|
|
|
// apply user provided positions once (new data has priority)
|
|
if (v.conf.positions) {
|
|
if (v.conf.positions.constructor === Array) {
|
|
v.conf.positions.forEach(function(n) {
|
|
if (v.data.idLookup[n.ID] !== undefined) {
|
|
if (!v.data.idLookup[n.ID].fixed) {
|
|
v.data.idLookup[n.ID].fixed = n.fixed;
|
|
}
|
|
if (!v.data.idLookup[n.ID].x) {
|
|
v.data.idLookup[n.ID].x = v.data.idLookup[n.ID].px = n.x;
|
|
}
|
|
if (!v.data.idLookup[n.ID].y) {
|
|
v.data.idLookup[n.ID].y = v.data.idLookup[n.ID].py = n.y;
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
v.tools.logError("Unable to set node positions: positions method parameter must be an array of " +
|
|
"node positions");
|
|
}
|
|
}
|
|
// apply old positions (new data has priority - if graph was ready, than user provided positions are
|
|
// already present in old positions) - see also graph.positions method
|
|
else if (v.status.graphOldPositions) {
|
|
v.status.graphOldPositions.forEach(function(n) {
|
|
if (v.data.idLookup[n.ID] !== undefined) {
|
|
if (!v.data.idLookup[n.ID].fixed) {
|
|
v.data.idLookup[n.ID].fixed = n.fixed;
|
|
}
|
|
if (!v.data.idLookup[n.ID].x) {
|
|
v.data.idLookup[n.ID].x = v.data.idLookup[n.ID].px = n.x;
|
|
}
|
|
if (!v.data.idLookup[n.ID].y) {
|
|
v.data.idLookup[n.ID].y = v.data.idLookup[n.ID].py = n.y;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
// clear positions
|
|
v.conf.positions = null;
|
|
v.status.graphOldPositions = null;
|
|
|
|
} //END: if (pData)
|
|
|
|
// set color and radius function and calculate nodes radius
|
|
v.tools.setColorFunction();
|
|
v.tools.setRadiusFunction();
|
|
v.data.nodes.forEach(function(n) {
|
|
n.radius = v.tools.radius(n.SIZEVALUE);
|
|
});
|
|
|
|
// MARKERS
|
|
v.main.markers = v.dom.defs.selectAll("marker.custom")
|
|
.data(v.data.distinctLinkColorValues,
|
|
function(m) {
|
|
return m;
|
|
}); // distinctLinkColorValues is a simple array, we return the "whole" color value string
|
|
v.main.markers.enter().append("svg:marker")
|
|
.attr("id", function(m) {
|
|
return v.dom.containerId + "_" + m;
|
|
})
|
|
.attr("class", "custom")
|
|
.attr("stroke", "none")
|
|
.attr("fill", function(m) {
|
|
return m;
|
|
})
|
|
.attr("viewBox", "0 0 10 10")
|
|
.attr("refX", 10)
|
|
.attr("refY", 5)
|
|
.attr("markerWidth", 5)
|
|
.attr("markerHeight", 5)
|
|
.attr("orient", "auto")
|
|
.attr("markerUnits", "strokeWidth")
|
|
.append("svg:path")
|
|
.attr("d", "M0,0 L10,5 L0,10");
|
|
v.main.markers.exit().remove();
|
|
|
|
// LINKS
|
|
v.main.links = v.dom.graph.selectAll("line.link")
|
|
.data(v.data.links.filter(function(l) {
|
|
return l.FROMID !== l.TOID;
|
|
}),
|
|
function(l) {
|
|
return l.FROMID + "_" + l.TOID;
|
|
});
|
|
v.main.links.enter().append("svg:line")
|
|
.attr("class", "link")
|
|
.on("mouseenter", v.tools.onLinkMouseenter)
|
|
.on("mouseleave", v.tools.onLinkMouseleave)
|
|
.on("click", v.tools.onLinkClick);
|
|
v.main.links.exit().remove();
|
|
// update all
|
|
v.main.links
|
|
.style("marker-end", v.tools.getMarkerUrl)
|
|
.classed("dotted", function(l) {
|
|
return (l.STYLE === "dotted");
|
|
})
|
|
.classed("dashed", function(l) {
|
|
return (l.STYLE === "dashed");
|
|
})
|
|
.style("stroke", function(l) {
|
|
return (l.COLOR ? l.COLOR : null);
|
|
});
|
|
|
|
// SELFLINKS
|
|
v.main.selfLinks = v.dom.graph.selectAll("path.link")
|
|
.data(v.data.links.filter(function(l) {
|
|
return l.FROMID === l.TOID && v.conf.showSelfLinks;
|
|
}),
|
|
function(l) {
|
|
return l.FROMID + "_" + l.TOID;
|
|
});
|
|
v.main.selfLinks.enter().append("svg:path")
|
|
.attr("id", function(l) {
|
|
return v.dom.containerId + "_link_" + l.FROMID + "_" + l.TOID;
|
|
})
|
|
.attr("class", "link")
|
|
.on("mouseenter", v.tools.onLinkMouseenter)
|
|
.on("mouseleave", v.tools.onLinkMouseleave)
|
|
.on("click", v.tools.onLinkClick);
|
|
v.main.selfLinks.exit().remove();
|
|
// update all
|
|
v.main.selfLinks
|
|
.attr("d", function(l) {
|
|
return v.tools.getSelfLinkPath(l);
|
|
})
|
|
.style("marker-end", v.tools.getMarkerUrl)
|
|
.classed("dotted", function(l) {
|
|
return (l.STYLE === "dotted");
|
|
})
|
|
.classed("dashed", function(l) {
|
|
return (l.STYLE === "dashed");
|
|
})
|
|
.style("stroke", function(l) {
|
|
return (l.COLOR ? l.COLOR : null);
|
|
});
|
|
|
|
// PATTERN for nodes with image attribute set
|
|
v.main.patterns = v.dom.defs.selectAll("pattern")
|
|
.data(v.data.nodes.filter(function(n) {
|
|
return (n.IMAGE ? true : false);
|
|
}),
|
|
function(n) {
|
|
return n.ID;
|
|
});
|
|
v.main.patterns.enter().append("svg:pattern")
|
|
.attr("id", function(n) {
|
|
return v.dom.containerId + "_pattern_" + n.ID;
|
|
})
|
|
.append("svg:image");
|
|
v.main.patterns.exit().remove();
|
|
// update all
|
|
v.main.patterns.each(function() {
|
|
d3.select(this)
|
|
.attr("x", 0)
|
|
.attr("y", 0)
|
|
.attr("height", function(n) {
|
|
return n.radius * 2;
|
|
})
|
|
.attr("width", function(n) {
|
|
return n.radius * 2;
|
|
});
|
|
d3.select(this.firstChild)
|
|
.attr("x", 0)
|
|
.attr("y", 0)
|
|
.attr("height", function(n) {
|
|
return n.radius * 2;
|
|
})
|
|
.attr("width", function(n) {
|
|
return n.radius * 2;
|
|
})
|
|
.attr("xlink:href", function(n) {
|
|
return n.IMAGE;
|
|
});
|
|
});
|
|
|
|
// NODES
|
|
v.main.nodes = v.dom.graph.selectAll("circle.node")
|
|
.data(v.data.nodes,
|
|
function(n) {
|
|
return n.ID;
|
|
});
|
|
v.main.nodes.enter().append("svg:circle")
|
|
.attr("class", "node")
|
|
.attr("cx", function(n) {
|
|
if (!n.fixed && !n.x) {
|
|
n.x = Math.floor((Math.random() * v.tools.getGraphWidth()) + 1);
|
|
return n.x;
|
|
}
|
|
})
|
|
.attr("cy", function(n) {
|
|
if (!n.fixed && !n.y) {
|
|
n.y = Math.floor((Math.random() * v.conf.height) + 1);
|
|
return n.y;
|
|
}
|
|
})
|
|
.on("mouseenter", v.tools.onNodeMouseenter)
|
|
.on("mouseleave", v.tools.onNodeMouseleave)
|
|
.on("click", v.tools.onNodeClick)
|
|
.on("dblclick", v.tools.onNodeDblclick)
|
|
.on("contextmenu", v.tools.onNodeContextmenu);
|
|
v.main.nodes.exit().remove();
|
|
// update all
|
|
v.main.nodes
|
|
.attr("r", function(n) {
|
|
return n.radius;
|
|
})
|
|
.attr("fill", function(n) {
|
|
return (n.IMAGE ? "url(#" + v.dom.containerId + "_pattern_" + n.ID + ")" : v.tools.color(n.COLORVALUE));
|
|
});
|
|
|
|
|
|
// LABELS
|
|
|
|
if (v.conf.showLabels) {
|
|
|
|
// normal text labels
|
|
v.main.labels = v.dom.graph.selectAll("text.label")
|
|
.data(v.data.nodes.filter(function(n) {
|
|
return !n.LABELCIRCULAR && !v.conf.labelsCircular;
|
|
}),
|
|
function(n) {
|
|
return n.ID;
|
|
});
|
|
v.main.labels.enter().append("svg:text")
|
|
.attr("class", "label");
|
|
v.main.labels.exit().remove();
|
|
// update all
|
|
v.main.labels.text(function(n) {
|
|
return n.LABEL;
|
|
});
|
|
|
|
// paths for circular labels
|
|
v.main.labelPaths = v.dom.defs.selectAll("path.label")
|
|
.data(v.data.nodes.filter(function(n) {
|
|
return n.LABELCIRCULAR || v.conf.labelsCircular;
|
|
}),
|
|
function(n) {
|
|
return n.ID;
|
|
});
|
|
v.main.labelPaths.enter().append("svg:path")
|
|
.attr("id", function(n) {
|
|
return v.dom.containerId + "_textPath_" + n.ID;
|
|
})
|
|
.attr("class", "label");
|
|
v.main.labelPaths.exit().remove();
|
|
// update all
|
|
v.main.labelPaths.attr("d", function(n) {
|
|
return v.tools.getLabelPath(n);
|
|
});
|
|
|
|
// circular labels
|
|
v.main.labelsCircular = v.dom.graph.selectAll("text.labelCircular")
|
|
.data(v.data.nodes.filter(function(n) {
|
|
return n.LABELCIRCULAR || v.conf.labelsCircular;
|
|
}),
|
|
function(n) {
|
|
return n.ID;
|
|
});
|
|
v.main.labelsCircular.enter().append("svg:text")
|
|
.attr("class", "labelCircular")
|
|
.append("svg:textPath")
|
|
.attr("xlink:href", function(n) {
|
|
return "#" + v.dom.containerId + "_textPath_" + n.ID;
|
|
});
|
|
v.main.labelsCircular.exit().remove();
|
|
// update all
|
|
v.main.labelsCircular.each(function(n) {
|
|
d3.select(this.firstChild).text(n.LABEL);
|
|
});
|
|
} else {
|
|
v.dom.defs.selectAll("path.label").remove();
|
|
v.dom.graph.selectAll("text.label,text.labelCircular").remove();
|
|
}
|
|
|
|
// initialize the graph (some options implicit initializes v.main.force, e.g. linkDistance, charge, ...)
|
|
graph
|
|
.debug(v.conf.debug)
|
|
.showBorder(v.conf.showBorder)
|
|
.setDomParentPaddingToZero(v.conf.setDomParentPaddingToZero)
|
|
.useDomParentWidth(v.conf.useDomParentWidth)
|
|
.width(v.conf.width)
|
|
.height(v.conf.height)
|
|
.alignFixedNodesToGrid(v.conf.alignFixedNodesToGrid)
|
|
.dragMode(v.conf.dragMode)
|
|
.pinMode(v.conf.pinMode)
|
|
.lassoMode(v.conf.lassoMode)
|
|
.zoomMode(v.conf.zoomMode)
|
|
.transform(v.conf.transform)
|
|
.autoRefresh(v.conf.autoRefresh)
|
|
.linkDistance(v.conf.linkDistance)
|
|
.charge(v.conf.charge)
|
|
.chargeDistance(v.conf.chargeDistance)
|
|
.gravity(v.conf.gravity)
|
|
.linkStrength(v.conf.linkStrength)
|
|
.friction(v.conf.friction)
|
|
.theta(v.conf.theta);
|
|
|
|
// start visualization
|
|
v.main.force
|
|
.nodes(v.data.nodes)
|
|
.links(v.data.links)
|
|
.start();
|
|
|
|
if (v.status.customize) {
|
|
v.tools.createCustomizeWizard();
|
|
} else {
|
|
v.tools.createCustomizeLink();
|
|
}
|
|
|
|
v.status.graphReady = true;
|
|
v.status.graphRendering = false;
|
|
|
|
v.tools.triggerApexEvent(document.querySelector("#" + v.dom.containerId), "apexafterrefresh");
|
|
|
|
return graph;
|
|
};
|
|
|
|
graph.resume = function() {
|
|
v.main.force.resume();
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
return graph;
|
|
};
|
|
|
|
graph.showBorder = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.showBorder;
|
|
}
|
|
v.conf.showBorder = value;
|
|
if (v.status.graphStarted) {
|
|
v.dom.svg.classed("border", v.conf.showBorder);
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.showLegend = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.showLegend;
|
|
}
|
|
v.conf.showLegend = value;
|
|
if (v.status.graphStarted) {
|
|
if (v.conf.showLegend) {
|
|
v.tools.removeLegend();
|
|
v.tools.createLegend();
|
|
} else {
|
|
v.tools.removeLegend();
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.showSelfLinks = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.showSelfLinks;
|
|
}
|
|
v.conf.showSelfLinks = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.showLinkDirection = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.showLinkDirection;
|
|
}
|
|
v.conf.showLinkDirection = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.showTooltips = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.showTooltips;
|
|
}
|
|
v.conf.showTooltips = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.tooltipPosition = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.tooltipPosition;
|
|
}
|
|
v.conf.tooltipPosition = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.colorScheme = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.colorScheme;
|
|
}
|
|
v.conf.colorScheme = value;
|
|
v.tools.setColorFunction();
|
|
if (v.status.graphStarted) {
|
|
v.main.nodes
|
|
.attr("fill", function(n) {
|
|
return (n.IMAGE ? "url(#" + v.dom.containerId + "_pattern_" + n.ID + ")" :
|
|
v.tools.color(n.COLORVALUE));
|
|
});
|
|
if (v.conf.showLegend) {
|
|
v.tools.removeLegend();
|
|
v.tools.createLegend();
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.showLabels = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.showLabels;
|
|
}
|
|
v.conf.showLabels = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.labelsCircular = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.labelsCircular;
|
|
}
|
|
v.conf.labelsCircular = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.preventLabelOverlappingOnForceEnd = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.preventLabelOverlappingOnForceEnd;
|
|
}
|
|
v.conf.preventLabelOverlappingOnForceEnd = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.labelPlacementIterations = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.labelPlacementIterations;
|
|
}
|
|
v.conf.labelPlacementIterations = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.dragMode = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.dragMode;
|
|
}
|
|
v.conf.dragMode = value;
|
|
if (v.status.graphStarted) {
|
|
if (v.conf.dragMode) {
|
|
v.main.nodes.call(v.main.drag);
|
|
} else {
|
|
// http://stackoverflow.com/questions/13136355/d3-js-remove-force-drag-from-a-selection
|
|
v.main.nodes.on("mousedown.drag", null);
|
|
v.main.nodes.on("touchstart.drag", null);
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.pinMode = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.pinMode;
|
|
}
|
|
v.conf.pinMode = value;
|
|
if (v.status.graphStarted) {
|
|
if (v.conf.pinMode) {
|
|
v.main.drag.on("dragstart", function(n) {
|
|
d3.select(this).classed("fixed", n.fixed = 1);
|
|
});
|
|
} else {
|
|
v.main.drag.on("dragstart", null);
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.lassoMode = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.lassoMode;
|
|
}
|
|
v.conf.lassoMode = value;
|
|
if (v.status.graphStarted) {
|
|
if (v.conf.lassoMode) {
|
|
v.dom.graphOverlay.call(v.main.lasso);
|
|
v.main.lasso.items(v.main.nodes);
|
|
v.main.lasso.on("start", function() {
|
|
v.main.lasso.items().classed("selected", false);
|
|
v.tools.onLassoStart(v.main.lasso.items());
|
|
});
|
|
v.main.lasso.on("draw", function() {
|
|
v.main.lasso.items().filter(function(d) {
|
|
return d.possible === true;
|
|
})
|
|
.classed("selected", true);
|
|
v.main.lasso.items().filter(function(d) {
|
|
return d.possible === false;
|
|
})
|
|
.classed("selected", false);
|
|
});
|
|
v.main.lasso.on("end", function() {
|
|
v.main.lasso.items().filter(function(d) {
|
|
return d.selected === true;
|
|
})
|
|
.classed("selected", true);
|
|
v.main.lasso.items().filter(function(d) {
|
|
return d.selected === false;
|
|
})
|
|
.classed("selected", false);
|
|
v.tools.onLassoEnd(v.main.lasso.items());
|
|
});
|
|
// save lasso event for use in event proxy
|
|
v.events.mousedownLasso = v.dom.graphOverlay.on("mousedown.drag");
|
|
v.events.touchstartLasso = v.dom.graphOverlay.on("touchstart.drag");
|
|
//v.events.touchmoveDrag = v.dom.graphOverlay.on("touchmove.drag");
|
|
//v.events.touchendDrag = v.dom.graphOverlay.on("touchend.drag");
|
|
|
|
// register event proxy for relevant lasso events who conflict with force functions -> see also
|
|
// v.tools.lassoEventProxy
|
|
v.dom.graphOverlay.on("mousedown.drag", v.tools.lassoEventProxy(v.events.mousedownLasso));
|
|
v.dom.graphOverlay.on("touchstart.drag", v.tools.lassoEventProxy(v.events.touchstartLasso));
|
|
//v.dom.graphOverlay.on("touchmove.drag", v.tools.lassoEventProxy(v.events.touchmoveDrag));
|
|
//v.dom.graphOverlay.on("touchend.drag", v.tools.lassoEventProxy(v.events.touchendDrag));
|
|
} else {
|
|
v.dom.graphOverlay.on(".drag", null);
|
|
v.main.nodes.classed("selected", false);
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.transform = function(value) {
|
|
if (!arguments.length) {
|
|
return {
|
|
"translate": v.main.zoom.translate(),
|
|
"scale": v.main.zoom.scale()
|
|
};
|
|
} else {
|
|
v.main.zoom.translate(value.translate);
|
|
v.main.zoom.scale(value.scale);
|
|
v.conf.transform = {
|
|
"translate": v.main.zoom.translate(),
|
|
"scale": v.main.zoom.scale()
|
|
};
|
|
if (v.conf.zoomMode && v.status.graphStarted) {
|
|
v.dom.graph.attr("transform", "translate(" + v.main.zoom.translate() + ")scale(" +
|
|
v.main.zoom.scale() + ")");
|
|
v.tools.writeConfObjectIntoWizard();
|
|
}
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.zoomMode = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.zoomMode;
|
|
}
|
|
v.conf.zoomMode = value;
|
|
if (v.status.graphStarted) {
|
|
if (v.conf.zoomMode) {
|
|
v.dom.graphOverlay.call(v.main.zoom);
|
|
v.main.zoom.scaleExtent([v.conf.minZoomFactor, v.conf.maxZoomFactor])
|
|
.size([v.tools.getGraphWidth(), v.conf.height])
|
|
.on("zoom", function() {
|
|
v.main.zoom.translate(d3.event.translate);
|
|
v.main.zoom.scale(d3.event.scale);
|
|
v.conf.transform = {
|
|
"translate": v.main.zoom.translate(),
|
|
"scale": v.main.zoom.scale()
|
|
};
|
|
v.dom.graph.attr("transform", "translate(" + v.main.zoom.translate() + ")scale(" +
|
|
v.main.zoom.scale() + ")");
|
|
v.tools.writeConfObjectIntoWizard();
|
|
});
|
|
// save zoom events for use in event proxy
|
|
v.events.dblclickZoom = v.dom.graphOverlay.on("dblclick.zoom");
|
|
v.events.mousedownZoom = v.dom.graphOverlay.on("mousedown.zoom");
|
|
v.events.touchstartZoom = v.dom.graphOverlay.on("touchstart.zoom");
|
|
//v.events.touchmoveZoom = v.dom.graphOverlay.on("touchmove.zoom");
|
|
//v.events.touchendZoom = v.dom.graphOverlay.on("touchend.zoom");
|
|
|
|
// register event proxy for relevant zoom events who conflicts with force functions -> see also
|
|
// v.tools.zoomEventProxy
|
|
v.dom.graphOverlay.on("dblclick.zoom", v.tools.zoomEventProxy(v.events.dblclickZoom));
|
|
v.dom.graphOverlay.on("mousedown.zoom", v.tools.zoomEventProxy(v.events.mousedownZoom));
|
|
v.dom.graphOverlay.on("touchstart.zoom", v.tools.zoomEventProxy(v.events.touchstartZoom));
|
|
//v.dom.graphOverlay.on("touchmove.zoom", v.tools.zoomEventProxy(v.events.touchmoveZoom));
|
|
//v.dom.graphOverlay.on("touchend.zoom", v.tools.zoomEventProxy(v.events.touchendZoom));
|
|
|
|
// transform graph, if conf is not default
|
|
if (JSON.stringify(v.conf.transform) !== JSON.stringify(v.confDefaults.transform)) {
|
|
v.dom.graph.attr("transform", "translate(" + v.main.zoom.translate() + ")scale(" +
|
|
v.main.zoom.scale() + ")");
|
|
v.tools.writeConfObjectIntoWizard();
|
|
}
|
|
} else {
|
|
// http://stackoverflow.com/questions/22302919/
|
|
// unregister-zoom-listener-and-restore-scroll-ability-in-d3-js/22303160?noredirect=1#22303160
|
|
v.dom.graphOverlay.on(".zoom", null);
|
|
v.main.zoom.translate([0, 0]);
|
|
v.main.zoom.scale(1);
|
|
v.conf.transform = {
|
|
"translate": [0, 0],
|
|
"scale": 1
|
|
};
|
|
v.dom.graph.attr("transform", "translate(0,0)scale(1)");
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.minZoomFactor = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.minZoomFactor;
|
|
}
|
|
v.conf.minZoomFactor = value;
|
|
if (v.status.graphReady) {
|
|
graph.zoomMode(v.conf.zoomMode);
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.maxZoomFactor = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.maxZoomFactor;
|
|
}
|
|
v.conf.maxZoomFactor = value;
|
|
if (v.status.graphReady) {
|
|
graph.zoomMode(v.conf.zoomMode);
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.zoom = function(centerX, centerY, viewportWidth) {
|
|
if (v.conf.zoomMode) {
|
|
var width = v.tools.getGraphWidth();
|
|
if (!centerX) {
|
|
centerX = width / 2;
|
|
}
|
|
if (!centerY) {
|
|
centerY = v.conf.height / 2;
|
|
}
|
|
if (!viewportWidth) {
|
|
viewportWidth = width;
|
|
}
|
|
v.main.zoom.scale(width / viewportWidth);
|
|
if (v.main.zoom.scale() < v.conf.minZoomFactor) {
|
|
v.main.zoom.scale(v.conf.minZoomFactor);
|
|
}
|
|
if (v.main.zoom.scale() > v.conf.maxZoomFactor) {
|
|
v.main.zoom.scale(v.conf.maxZoomFactor);
|
|
}
|
|
v.main.zoom.translate([
|
|
(width / 2 - centerX * v.main.zoom.scale()), (v.conf.height / 2 - centerY * v.main.zoom.scale())
|
|
]);
|
|
v.conf.transform = {
|
|
"translate": v.main.zoom.translate(),
|
|
"scale": v.main.zoom.scale()
|
|
};
|
|
v.main.zoom.event(v.dom.graphOverlay);
|
|
v.tools.writeConfObjectIntoWizard();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.zoomSmooth = function(centerX, centerY, viewportWidth, duration) {
|
|
if (v.conf.zoomMode) {
|
|
var width = v.tools.getGraphWidth();
|
|
if (!centerX) {
|
|
centerX = width / 2;
|
|
}
|
|
if (!centerY) {
|
|
centerY = v.conf.height / 2;
|
|
}
|
|
if (!viewportWidth) {
|
|
viewportWidth = width;
|
|
}
|
|
if (!duration) {
|
|
duration = 1500;
|
|
}
|
|
v.main.zoom.scale(width / viewportWidth);
|
|
if (v.main.zoom.scale() < v.conf.minZoomFactor) {
|
|
v.main.zoom.scale(v.conf.minZoomFactor);
|
|
}
|
|
if (v.main.zoom.scale() > v.conf.maxZoomFactor) {
|
|
v.main.zoom.scale(v.conf.maxZoomFactor);
|
|
}
|
|
v.main.zoom.translate([
|
|
(width / 2 - centerX * v.main.zoom.scale()), (v.conf.height / 2 - centerY * v.main.zoom.scale())
|
|
]);
|
|
v.conf.translate = {
|
|
"translate": v.main.zoom.translate(),
|
|
"scale": v.main.zoom.scale()
|
|
};
|
|
v.dom.graphOverlay.transition()
|
|
.duration(duration)
|
|
.call(v.main.zoom.event);
|
|
v.tools.writeConfObjectIntoWizard();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.showLoadingIndicatorOnAjaxCall = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.showLoadingIndicatorOnAjaxCall;
|
|
}
|
|
v.conf.showLoadingIndicatorOnAjaxCall = value;
|
|
return graph;
|
|
};
|
|
|
|
graph.showLoadingIndicator = function(value) {
|
|
if (v.tools.parseBool(value)) {
|
|
v.dom.loading.style("display", "block");
|
|
} else {
|
|
v.dom.loading.style("display", "none");
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.alignFixedNodesToGrid = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.alignFixedNodesToGrid;
|
|
}
|
|
v.conf.alignFixedNodesToGrid = value;
|
|
if (v.status.graphStarted) {
|
|
// align fixed nodes to grid
|
|
if (v.conf.alignFixedNodesToGrid) {
|
|
// NO aligning on the very first start: this would overwrite user defined positions
|
|
if (v.status.graphReady) {
|
|
v.main.nodes.each(function(n) {
|
|
if (n.fixed) {
|
|
n.x = n.px = v.tools.getNearestGridPosition(n.x, v.conf.width);
|
|
n.y = n.py = v.tools.getNearestGridPosition(n.y, v.conf.height);
|
|
}
|
|
});
|
|
}
|
|
v.main.drag.on("dragend", function(n) {
|
|
n.x = n.px = v.tools.getNearestGridPosition(n.x, v.conf.width);
|
|
n.y = n.py = v.tools.getNearestGridPosition(n.y, v.conf.height);
|
|
});
|
|
} else {
|
|
v.main.drag.on("dragend", null);
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.moveFixedNodes = function(x, y) {
|
|
if (v.status.graphStarted) {
|
|
if (!x) {
|
|
x = 0;
|
|
}
|
|
if (!y) {
|
|
y = 0;
|
|
}
|
|
if (x !== 0 || y !== 0) {
|
|
v.main.nodes.each(function(n) {
|
|
if (n.fixed) {
|
|
n.x = n.px = (v.conf.alignFixedNodesToGrid ?
|
|
v.tools.getNearestGridPosition(n.x + x, v.conf.width) : n.x + x);
|
|
n.y = n.py = (v.conf.alignFixedNodesToGrid ?
|
|
v.tools.getNearestGridPosition(n.y + y, v.conf.width) : n.y + y);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.gridSize = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.gridSize;
|
|
}
|
|
v.conf.gridSize = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.nodeEventToStopPinMode = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.nodeEventToStopPinMode;
|
|
}
|
|
v.conf.nodeEventToStopPinMode = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.onNodeContextmenuPreventDefault = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.onNodeContextmenuPreventDefault;
|
|
}
|
|
v.conf.onNodeContextmenuPreventDefault = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.nodeEventToOpenLink = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.nodeEventToOpenLink;
|
|
}
|
|
v.conf.nodeEventToOpenLink = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.nodeLinkTarget = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.nodeLinkTarget;
|
|
}
|
|
v.conf.nodeLinkTarget = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.autoRefresh = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.autoRefresh;
|
|
}
|
|
v.conf.autoRefresh = value;
|
|
if (v.status.graphStarted) {
|
|
if (v.conf.autoRefresh && v.conf.refreshInterval && !v.conf.interval) {
|
|
v.conf.interval = window.setInterval(function() {
|
|
graph.start();
|
|
}, v.conf.refreshInterval);
|
|
v.tools.log("Auto refresh started with an interval of " + v.conf.refreshInterval + " milliseconds.");
|
|
} else if (!v.conf.autoRefresh && v.conf.interval) {
|
|
clearInterval(v.conf.interval);
|
|
v.conf.interval = null;
|
|
v.tools.log("Auto refresh stopped.");
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.refreshInterval = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.refreshInterval;
|
|
}
|
|
v.conf.refreshInterval = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.useDomParentWidth = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.useDomParentWidth;
|
|
}
|
|
v.conf.useDomParentWidth = value;
|
|
if (v.status.graphStarted) {
|
|
if (v.conf.useDomParentWidth) {
|
|
v.dom.containerWidth = v.tools.getSvgParentInnerWidth();
|
|
d3.select(window).on("resize", function() {
|
|
var oldWidth = v.dom.containerWidth;
|
|
var newWidth = v.tools.getSvgParentInnerWidth();
|
|
if (oldWidth !== newWidth) {
|
|
v.dom.containerWidth = newWidth;
|
|
graph.width(v.conf.width).resume();
|
|
}
|
|
});
|
|
} else {
|
|
d3.select(window).on("resize", null);
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.setDomParentPaddingToZero = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.setDomParentPaddingToZero;
|
|
}
|
|
v.conf.setDomParentPaddingToZero = value;
|
|
if (v.status.graphStarted) {
|
|
if (v.conf.setDomParentPaddingToZero) {
|
|
v.dom.svgParent.style("padding", "0");
|
|
} else {
|
|
v.dom.svgParent.style("padding", null);
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.domParentWidth = function() {
|
|
return v.dom.containerWidth || v.tools.getSvgParentInnerWidth();
|
|
};
|
|
|
|
graph.width = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.width;
|
|
}
|
|
v.conf.width = value;
|
|
if (v.status.graphStarted) {
|
|
v.dom.svg.attr("width", v.tools.getGraphWidth());
|
|
v.dom.graphOverlaySizeHelper.attr("width", v.tools.getGraphWidth());
|
|
v.dom.loadingRect.attr("width", v.tools.getGraphWidth());
|
|
v.dom.loadingText.attr("x", v.tools.getGraphWidth() / 2);
|
|
v.main.force.size([v.tools.getGraphWidth(), v.conf.height]);
|
|
if (v.conf.zoomMode) {
|
|
v.main.zoom.size([v.tools.getGraphWidth(), v.conf.height]);
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.height = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.height;
|
|
}
|
|
v.conf.height = value;
|
|
if (v.status.graphStarted) {
|
|
v.dom.svg.attr("height", v.conf.height);
|
|
v.dom.graphOverlaySizeHelper.attr("height", v.conf.height);
|
|
v.dom.loadingRect.attr("height", v.conf.height);
|
|
v.dom.loadingText.attr("y", v.conf.height / 2);
|
|
v.main.force.size([v.tools.getGraphWidth(), v.conf.height]);
|
|
if (v.conf.showLegend) {
|
|
v.tools.removeLegend();
|
|
v.tools.createLegend();
|
|
}
|
|
if (v.conf.zoomMode) {
|
|
v.main.zoom.size([v.tools.getGraphWidth(), v.conf.height]);
|
|
}
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.minNodeRadius = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.minNodeRadius;
|
|
}
|
|
v.conf.minNodeRadius = value;
|
|
if (v.status.graphReady) {
|
|
v.tools.setRadiusFunction();
|
|
v.main.nodes.each(function(n) {
|
|
n.radius = v.tools.radius(n.SIZEVALUE);
|
|
});
|
|
v.main.nodes.attr("r", function(n) {
|
|
return n.radius;
|
|
});
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.maxNodeRadius = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.maxNodeRadius;
|
|
}
|
|
v.conf.maxNodeRadius = value;
|
|
if (v.status.graphReady) {
|
|
v.tools.setRadiusFunction();
|
|
v.main.nodes.each(function(n) {
|
|
n.radius = v.tools.radius(n.SIZEVALUE);
|
|
});
|
|
v.main.nodes.attr("r", function(n) {
|
|
return n.radius;
|
|
});
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.labelDistance = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.labelDistance;
|
|
}
|
|
v.conf.labelDistance = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.selfLinkDistance = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.selfLinkDistance;
|
|
}
|
|
v.conf.selfLinkDistance = value;
|
|
if (v.status.graphStarted) {
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.linkDistance = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.linkDistance;
|
|
}
|
|
v.conf.linkDistance = value;
|
|
if (v.status.graphStarted) {
|
|
v.main.force.linkDistance(v.conf.linkDistance);
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.charge = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.charge;
|
|
}
|
|
v.conf.charge = value;
|
|
if (v.status.graphStarted) {
|
|
v.main.force.charge(v.conf.charge);
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.chargeDistance = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.chargeDistance;
|
|
}
|
|
v.conf.chargeDistance = value;
|
|
if (v.status.graphStarted) {
|
|
v.main.force.chargeDistance(v.conf.chargeDistance);
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.gravity = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.gravity;
|
|
}
|
|
v.conf.gravity = value;
|
|
if (v.status.graphStarted) {
|
|
v.main.force.gravity(v.conf.gravity);
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.linkStrength = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.linkStrength;
|
|
}
|
|
v.conf.linkStrength = value;
|
|
if (v.status.graphStarted) {
|
|
v.main.force.linkStrength(v.conf.linkStrength);
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.friction = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.friction;
|
|
}
|
|
v.conf.friction = value;
|
|
if (v.status.graphStarted) {
|
|
v.main.force.friction(v.conf.friction);
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.theta = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.theta;
|
|
}
|
|
v.conf.theta = value;
|
|
if (v.status.graphStarted) {
|
|
v.main.force.theta(v.conf.theta);
|
|
v.tools.createCustomizeWizardIfNotRendering();
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.positions = function(value) {
|
|
if (!arguments.length) {
|
|
var positions = [];
|
|
v.data.nodes.forEach(function(n) {
|
|
positions.push({
|
|
"ID": n.ID,
|
|
"x": Math.round(n.x),
|
|
"y": Math.round(n.y),
|
|
"fixed": (n.fixed ? 1 : 0)
|
|
});
|
|
});
|
|
return positions;
|
|
} else {
|
|
if (v.status.graphReady) {
|
|
if (value.constructor === Array) {
|
|
value.forEach(function(n) {
|
|
if (v.data.idLookup[n.ID] !== undefined) {
|
|
v.data.idLookup[n.ID].fixed = v.tools.parseBool(n.fixed);
|
|
v.data.idLookup[n.ID].x = v.data.idLookup[n.ID].px = n.x;
|
|
v.data.idLookup[n.ID].y = v.data.idLookup[n.ID].py = n.y;
|
|
}
|
|
});
|
|
} else {
|
|
v.tools.logError("Unable to set node positions: positions method parameter must be an array of " +
|
|
"node positions");
|
|
}
|
|
} else {
|
|
v.conf.positions = value; // we do positioning later after start() is called
|
|
}
|
|
return graph;
|
|
}
|
|
};
|
|
|
|
graph.onLinkClickFunction = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.onLinkClickFunction;
|
|
}
|
|
v.conf.onLinkClickFunction = value;
|
|
return graph;
|
|
};
|
|
|
|
graph.onNodeMouseenterFunction = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.onNodeMouseenterFunction;
|
|
}
|
|
v.conf.onNodeMouseenterFunction = value;
|
|
return graph;
|
|
};
|
|
|
|
graph.onNodeMouseleaveFunction = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.onNodeMouseleaveFunction;
|
|
}
|
|
v.conf.onNodeMouseleaveFunction = value;
|
|
return graph;
|
|
};
|
|
|
|
graph.onNodeClickFunction = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.onNodeClickFunction;
|
|
}
|
|
v.conf.onNodeClickFunction = value;
|
|
return graph;
|
|
};
|
|
|
|
graph.onNodeDblclickFunction = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.onNodeDblclickFunction;
|
|
}
|
|
v.conf.onNodeDblclickFunction = value;
|
|
return graph;
|
|
};
|
|
|
|
graph.onNodeContextmenuFunction = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.onNodeContextmenuFunction;
|
|
}
|
|
v.conf.onNodeContextmenuFunction = value;
|
|
return graph;
|
|
};
|
|
|
|
graph.onLassoStartFunction = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.onLassoStartFunction;
|
|
}
|
|
v.conf.onLassoStartFunction = value;
|
|
return graph;
|
|
};
|
|
|
|
graph.onLassoEndFunction = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.onLassoEndFunction;
|
|
}
|
|
v.conf.onLassoEndFunction = value;
|
|
return graph;
|
|
};
|
|
|
|
graph.sampleData = function(value) {
|
|
if (!arguments.length) {
|
|
return v.data.sampleData;
|
|
}
|
|
v.data.sampleData = value;
|
|
return graph;
|
|
};
|
|
|
|
graph.data = function() {
|
|
return v.data.dataConverted;
|
|
};
|
|
|
|
graph.nodeDataById = function(ID) {
|
|
return v.data.idLookup[ID];
|
|
};
|
|
|
|
graph.options = function(value) {
|
|
var key;
|
|
if (!arguments.length) {
|
|
var conf = {};
|
|
for (key in v.conf) {
|
|
if (v.conf.hasOwnProperty(key)) {
|
|
if (v.confDefaults.hasOwnProperty(key)) {
|
|
if ((v.confDefaults[key].type === "bool" ||
|
|
v.confDefaults[key].type === "number" ||
|
|
v.confDefaults[key].type === "text") &&
|
|
v.confDefaults[key].val !== v.conf[key]) {
|
|
conf[key] = v.conf[key];
|
|
} else if (v.confDefaults[key].type === "object" &&
|
|
JSON.stringify(v.confDefaults[key].val) !== JSON.stringify(v.conf[key])) {
|
|
conf[key] = v.conf[key];
|
|
}
|
|
} else if (!v.confDefaults.hasOwnProperty(key) &&
|
|
v.conf[key] !== undefined &&
|
|
v.conf[key] !== null) {
|
|
conf[key] = v.conf[key];
|
|
}
|
|
}
|
|
}
|
|
return conf;
|
|
} else {
|
|
v.tools.applyConfigurationObject(value);
|
|
return graph;
|
|
}
|
|
};
|
|
|
|
graph.optionsCustomizationWizard = function(value) {
|
|
var key;
|
|
if (!arguments.length) {
|
|
var conf = {};
|
|
for (key in v.confDefaults) {
|
|
if (v.confDefaults.hasOwnProperty(key)) {
|
|
if ((v.confDefaults[key].type === "bool" ||
|
|
v.confDefaults[key].type === "number" ||
|
|
v.confDefaults[key].type === "text") &&
|
|
v.confDefaults[key].val !== v.conf[key]) {
|
|
conf[key] = v.conf[key];
|
|
} else if (v.confDefaults[key].type === "object" &&
|
|
JSON.stringify(v.confDefaults[key].val) !== JSON.stringify(v.conf[key])) {
|
|
conf[key] = v.conf[key];
|
|
}
|
|
}
|
|
}
|
|
return conf;
|
|
} else {
|
|
v.tools.applyConfigurationObject(value);
|
|
return graph;
|
|
}
|
|
};
|
|
|
|
graph.customize = function(value) {
|
|
if (!arguments.length) {
|
|
return v.status.customize;
|
|
}
|
|
v.status.customize = value;
|
|
if (v.status.graphStarted) {
|
|
if (v.status.customize) {
|
|
v.tools.createCustomizeWizard();
|
|
} else {
|
|
v.tools.removeCustomizeWizard();
|
|
}
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.debug = function(value) {
|
|
if (!arguments.length) {
|
|
return v.conf.debug;
|
|
}
|
|
v.conf.debug = value;
|
|
if (v.status.graphStarted) {
|
|
if (v.conf.debug) {
|
|
v.tools.createCustomizeLink();
|
|
} else {
|
|
v.tools.removeCustomizeLink();
|
|
}
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.releaseFixedNodes = function() {
|
|
if (v.status.graphStarted) {
|
|
v.main.nodes.each(function(n) {
|
|
n.fixed = 0;
|
|
});
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
graph.userAgent = function() {
|
|
return v.status.userAgent;
|
|
};
|
|
|
|
// public inspect function: to inspect the global object, which holds all data, functions and references
|
|
graph.inspect = function() {
|
|
return v;
|
|
};
|
|
|
|
graph.version = function() {
|
|
return v.version;
|
|
};
|
|
|
|
/*******************************************************************************************************************
|
|
* Startup code - runs one time after the initialization of a new chart - example:
|
|
* var myChart = net_gobrechts_d3_force( pDomContainerId, pConf, pApexPluginId ).start();
|
|
*/
|
|
|
|
// bind to the apexrefresh event, so that this region can be refreshed by a dynamic action
|
|
if (v.status.apexPluginId) {
|
|
apex.jQuery("#" + v.dom.containerId).bind("apexrefresh", function() {
|
|
graph.start();
|
|
});
|
|
}
|
|
|
|
// final return
|
|
return graph;
|
|
|
|
}
|