src/graph/node/node.js
import * as d3 from "d3";
import runner from "../../runner/runner";
import result from "../../result/result";
import cypherviewer from "../../cypherviewer/cypherviewer";
import graph from "../graph";
import provider from "../../provider/provider";
import fitTextRenderer from "./fitTextRenderer";
import dataModel from "../../datamodel/dataModel";
import {update} from "../../popoto";
import queryviewer from "../../queryviewer/queryviewer";
import textRenderer from "./textRenderer";
import logger from "../../logger/logger";
import query from "../../query/query";
var node = {};
// ID of the g element in SVG graph containing all the link elements.
node.gID = "popoto-gnodes";
// Node ellipse size used by default for text nodes.
node.DONUTS_MARGIN = 0;
node.DONUT_WIDTH = 20;
// Define the max number of character displayed in node.
node.NODE_MAX_CHARS = 11;
node.NODE_TITLE_MAX_CHARS = 100;
// Number of nodes displayed per page during value selection.
node.PAGE_SIZE = 10;
// Count box default size
node.CountBox = {x: 16, y: 33, w: 52, h: 19};
// Store choose node state to avoid multiple node expand at the same time
node.chooseWaiting = false;
node.getDonutInnerRadius = function (n) {
return provider.node.getSize(n) + node.DONUTS_MARGIN;
};
node.getDonutOuterRadius = function (n) {
return provider.node.getSize(n) + node.DONUTS_MARGIN + node.DONUT_WIDTH;
};
node.pie = d3.pie()
.sort(null)
.value(function (d) {
return 1;
});
/**
* Defines the list of possible nodes.
* ROOT: Node used as graph root. It is the target of the query. Only one node of this type should be available in graph.
* CHOOSE: Nodes defining a generic node label. From these node is is possible to select a value or explore relations.
* VALUE: Unique node containing a value constraint. Usually replace CHOOSE nodes once a value as been selected.
* GROUP: Empty node used to group relations. No value can be selected but relations can be explored. These nodes doesn't have count.
*/
node.NodeTypes = Object.freeze({ROOT: 0, CHOOSE: 1, VALUE: 2, GROUP: 3});
// Used to generate unique internal labels used for example as identifier in Cypher query.
node.internalLabels = {};
/**
* Create a normalized identifier from a node label.
* Multiple calls with the same node label will generate different unique identifier.
*
* @param nodeLabel
* @returns {string}
*/
node.generateInternalLabel = function (nodeLabel) {
var label = nodeLabel ? nodeLabel.toLowerCase().replace(/ /g, '') : "n";
if (label in node.internalLabels) {
node.internalLabels[label] = node.internalLabels[label] + 1;
} else {
node.internalLabels[label] = 0;
return label;
}
return label + node.internalLabels[label];
};
/**
* Update Nodes SVG elements using D3.js update mechanisms.
*/
node.updateNodes = function () {
var data = node.updateData();
node.removeElements(data.exit());
node.addNewElements(data.enter());
node.updateElements();
};
/**
* Update node data with changes done in dataModel.nodes model.
*/
node.updateData = function () {
var data = graph.svg.select("#" + node.gID).selectAll(".ppt-gnode").data(dataModel.nodes, function (d) {
return d.id;
});
if (graph.hasGraphChanged) {
node.updateAutoLoadValues();
if (!graph.DISABLE_COUNT && !graph.ignoreCount) {
node.updateCount();
}
}
graph.hasGraphChanged = false;
return data;
};
/**
* Update nodes and result counts by executing a query for every nodes with the new graph structure.
*/
node.updateCount = function () {
var statements = [];
var countedNodes = dataModel.nodes
.filter(function (d) {
return d.type !== node.NodeTypes.VALUE && d.type !== node.NodeTypes.GROUP && (!d.hasOwnProperty("isNegative") || !d.isNegative);
});
countedNodes.forEach(function (n) {
var nodeCountQuery = query.generateNodeCountQuery(n);
statements.push(
{
"statement": nodeCountQuery.statement,
"parameters": nodeCountQuery.parameters
}
);
});
logger.info("Count nodes ==>");
runner.run(
{
"statements": statements
})
.then(function (results) {
logger.info("<== Count nodes");
var data = runner.toObject(results);
for (var i = 0; i < countedNodes.length; i++) {
countedNodes[i].count = data[i][0].count;
}
// Update result count with root node new count
if (result.resultCountListeners.length > 0) {
result.updateResultsCount();
}
node.updateElements();
graph.link.updateElements();
})
.catch(function (error) {
logger.error(error);
countedNodes.forEach(function (n) {
n.count = 0;
});
node.updateElements();
graph.link.updateElements();
});
};
/**
* Update values for nodes having preloadData property
*/
node.updateAutoLoadValues = function () {
var statements = [];
var nodesToLoadData = node.getAutoLoadValueNodes();
for (var i = 0; i < nodesToLoadData.length; i++) {
var nodeToQuery = nodesToLoadData[i];
var nodeValueQuery = query.generateNodeValueQuery(nodeToQuery);
statements.push(
{
"statement": nodeValueQuery.statement,
"parameters": nodeValueQuery.parameters
}
);
}
if (statements.length > 0) {
logger.info("AutoLoadValue ==>");
runner.run(
{
"statements": statements
})
.then(function (results) {
logger.info("<== AutoLoadValue");
var data = runner.toObject(results)
for (var i = 0; i < nodesToLoadData.length; i++) {
var nodeToQuery = nodesToLoadData[i];
var constraintAttr = provider.node.getConstraintAttribute(nodeToQuery.label);
// Here results are parsed and values already selected are filtered out
nodeToQuery.data = data[i].filter(function (dataToFilter) {
var keepData = true;
if (nodeToQuery.hasOwnProperty("value") && nodeToQuery.value.length > 0) {
nodeToQuery.value.forEach(function (value) {
if (value.attributes[constraintAttr] === dataToFilter[constraintAttr]) {
keepData = false;
}
})
}
return keepData;
});
nodeToQuery.page = 1;
}
graph.notifyListeners(graph.Events.GRAPH_NODE_DATA_LOADED, [nodesToLoadData]);
})
.catch(function (error) {
logger.error(error);
});
}
};
/**
* Remove old elements.
* Should be called after updateData.
*/
node.removeElements = function (exitingData) {
// Nodes without parent are simply removed.
exitingData.filter(function (d) {
return !d.parent;
}).remove();
// Nodes with a parent are removed with an animation (nodes are collapsed to their parents before being removed)
exitingData.filter(function (d) {
return d.parent;
}).transition().duration(300).attr("transform", function (d) {
return "translate(" + d.parent.x + "," + d.parent.y + ")";
}).remove();
};
/**
* Add all new elements.
* Only the skeleton of new nodes are added custom data will be added during the element update phase.
* Should be called after updateData and before updateElements.
*/
node.addNewElements = function (enteringData) {
var gNewNodeElements = enteringData
.append("g")
.attr("class", "ppt-gnode");
gNewNodeElements.on("click", node.nodeClick)
.on("mouseover", node.mouseOverNode)
// .on("mousemove", nUdeXXX.mouseMoveNode)
.on("mouseout", node.mouseOutNode);
// Add right click on all nodes except value
gNewNodeElements.filter(function (d) {
return d.type !== node.NodeTypes.VALUE;
}).on("contextmenu", node.clearSelection);
// Disable right click context menu on value nodes
gNewNodeElements.filter(function (d) {
return d.type === node.NodeTypes.VALUE;
}).on("contextmenu", function (event) {
// Disable context menu on
event.preventDefault();
});
var nodeDefs = gNewNodeElements.append("defs");
// Circle clipPath using node radius size
nodeDefs.append("clipPath")
.attr("id", function (n) {
return "node-view" + n.id;
})
.append("circle")
.attr("cx", 0)
.attr("cy", 0);
// Nodes are composed of 3 layouts and skeleton are created here.
node.addBackgroundElements(gNewNodeElements);
node.addMiddlegroundElements(gNewNodeElements);
node.addForegroundElements(gNewNodeElements);
};
/**
* Create the background for a new node element.
* The background of a node is defined by a circle not visible by default (fill-opacity set to 0) but can be used to highlight a node with animation on this attribute.
* This circle also define the node zone that can receive events like mouse clicks.
*
* @param gNewNodeElements
*/
node.addBackgroundElements = function (gNewNodeElements) {
var background = gNewNodeElements
.append("g")
.attr("class", "ppt-g-node-background")
.classed("hide", graph.DISABLE_RELATION);
background.append("g")
.attr("class", "ppt-donut-labels");
background.append("g")
.attr("class", "ppt-donut-segments");
};
/**
* Create the node main elements.
*
* @param gNewNodeElements
*/
node.addMiddlegroundElements = function (gNewNodeElements) {
var middle = gNewNodeElements
.append("g")
.attr("class", "ppt-g-node-middleground");
};
/**
* Create the node foreground elements.
* It contains node additional elements, count or tools like navigation arrows.
*
* @param gNewNodeElements
*/
node.addForegroundElements = function (gNewNodeElements) {
var foreground = gNewNodeElements
.append("g")
.attr("class", "ppt-g-node-foreground");
// Arrows icons added only for root and choose nodes
var gArrow = foreground.filter(function (d) {
return d.type === node.NodeTypes.ROOT || d.type === node.NodeTypes.CHOOSE;
})
.append("g")
.attr("class", "ppt-node-foreground-g-arrows");
var glArrow = gArrow.append("g");
//glArrow.append("polygon")
//.attr("points", "-53,-23 -33,-33 -33,-13");
glArrow.append("circle")
.attr("class", "ppt-larrow")
.attr("cx", "-43")
.attr("cy", "-23")
.attr("r", "17");
glArrow.append("path")
.attr("class", "ppt-arrow")
.attr("d", "m -44.905361,-23 6.742,-6.742 c 0.81,-0.809 0.81,-2.135 0,-2.944 l -0.737,-0.737 c -0.81,-0.811 -2.135,-0.811 -2.945,0 l -8.835,8.835 c -0.435,0.434 -0.628,1.017 -0.597,1.589 -0.031,0.571 0.162,1.154 0.597,1.588 l 8.835,8.834 c 0.81,0.811 2.135,0.811 2.945,0 l 0.737,-0.737 c 0.81,-0.808 0.81,-2.134 0,-2.943 l -6.742,-6.743 z");
glArrow.on("click", function (event, clickedNode) {
event.stopPropagation(); // To avoid click event on svg element in background
// On left arrow click page number is decreased and node expanded to display the new page
if (clickedNode.page > 1) {
clickedNode.page--;
node.collapseNode(clickedNode);
node.expandNode(clickedNode);
}
});
var grArrow = gArrow.append("g");
//grArrow.append("polygon")
//.attr("points", "53,-23 33,-33 33,-13");
grArrow.append("circle")
.attr("class", "ppt-rarrow")
.attr("cx", "43")
.attr("cy", "-23")
.attr("r", "17");
grArrow.append("path")
.attr("class", "ppt-arrow")
.attr("d", "m 51.027875,-24.5875 -8.835,-8.835 c -0.811,-0.811 -2.137,-0.811 -2.945,0 l -0.738,0.737 c -0.81,0.81 -0.81,2.136 0,2.944 l 6.742,6.742 -6.742,6.742 c -0.81,0.81 -0.81,2.136 0,2.943 l 0.737,0.737 c 0.81,0.811 2.136,0.811 2.945,0 l 8.835,-8.836 c 0.435,-0.434 0.628,-1.017 0.597,-1.588 0.032,-0.569 -0.161,-1.152 -0.596,-1.586 z");
grArrow.on("click", function (event, clickedNode) {
event.stopPropagation(); // To avoid click event on svg element in background
if (clickedNode.page * node.PAGE_SIZE < clickedNode.count) {
clickedNode.page++;
node.collapseNode(clickedNode);
node.expandNode(clickedNode);
}
});
// Count box
if (!graph.DISABLE_COUNT) {
var countForeground = foreground.filter(function (d) {
return d.type !== node.NodeTypes.GROUP;
});
countForeground
.append("rect")
.attr("x", node.CountBox.x)
.attr("y", node.CountBox.y)
.attr("width", node.CountBox.w)
.attr("height", node.CountBox.h)
.attr("class", "ppt-count-box");
countForeground
.append("text")
.attr("x", 42)
.attr("y", 48)
.attr("text-anchor", "middle")
.attr("class", "ppt-count-text");
}
var ban = foreground.filter(function (d) {
return d.type === node.NodeTypes.CHOOSE;
}).append("g")
.attr("class", "ppt-g-node-ban")
.append("path")
.attr("d", "M89.1 19.2C88 17.7 86.6 16.2 85.2 14.8 83.8 13.4 82.3 12 80.8 10.9 72 3.9 61.3 0 50 0 36.7 0 24.2 5.4 14.8 14.8 5.4 24.2 0 36.7 0 50c0 11.4 3.9 22.1 10.9 30.8 1.2 1.5 2.5 3 3.9 4.4 1.4 1.4 2.9 2.7 4.4 3.9C27.9 96.1 38.6 100 50 100 63.3 100 75.8 94.6 85.2 85.2 94.6 75.8 100 63.3 100 50 100 38.7 96.1 28 89.1 19.2ZM11.9 50c0-10.2 4-19.7 11.1-27C30.3 15.9 39.8 11.9 50 11.9c8.2 0 16 2.6 22.4 7.3L19.3 72.4C14.5 66 11.9 58.2 11.9 50Zm65 27c-7.2 7.1-16.8 11.1-27 11.1-8.2 0-16-2.6-22.4-7.4L80.8 27.6C85.5 34 88.1 41.8 88.1 50c0 10.2-4 19.7-11.1 27z");
};
/**
* Updates all elements.
*/
node.updateElements = function () {
var toUpdateElem = graph.svg.select("#" + node.gID).selectAll(".ppt-gnode");
toUpdateElem.attr("id", function (d) {
return "popoto-gnode_" + d.id;
});
if (graph.USE_VORONOI_LAYOUT) {
toUpdateElem.attr("clip-path", function (d) {
return "url(#voroclip-" + d.id + ")";
});
}
toUpdateElem.select("defs")
.select("clipPath")
.attr("id", function (n) {
return "node-view" + n.id;
}).selectAll("circle")
.attr("r", function (n) {
return provider.node.getSize(n);
});
// TODO ZZZ move functions?
toUpdateElem.filter(function (n) {
return n.type !== node.NodeTypes.ROOT
}).call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function dragstarted(event, d) {
if (!event.active) graph.force.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) graph.force.alphaTarget(0);
if (d.fixed === false) {
d.fx = null;
d.fy = null;
}
}
node.updateBackgroundElements();
node.updateMiddlegroundElements();
node.updateForegroundElements();
};
node.updateBackgroundElements = function () {
var nodeBackgroundElements = graph.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-background");
nodeBackgroundElements.select(".ppt-donut-labels").selectAll("*").remove();
nodeBackgroundElements.select(".ppt-donut-segments").selectAll("*").remove();
var gSegment = nodeBackgroundElements.select(".ppt-donut-segments").selectAll(".ppt-segment-container")
.data(function (d) {
var relationships = [];
if (d.hasOwnProperty("relationships")) {
relationships = d.relationships;
}
return relationships;
}, function (d) {
return d.id;
})
.enter()
.append("g")
.attr("class", ".ppt-segment-container")
.on("click", node.segmentClick)
.on("mouseover", function (d) {
d3.select(this).select(".ppt-text-arc").classed("hover", true)
})
.on("mouseout", function (d) {
d3.select(this).select(".ppt-text-arc").classed("hover", false)
});
gSegment.append("title").attr("class", "ppt-svg-title")
.text(function (d) {
return d.label + " " + d.target;
});
var gLabel = nodeBackgroundElements.select(".ppt-donut-labels").selectAll(".ppt-segment-container")
.data(function (n) {
var relationships = [];
if (n.hasOwnProperty("relationships")) {
relationships = n.relationships;
}
return relationships;
}, function (relationship) {
return relationship.id;
})
.enter()
.append("g")
.attr("class", ".ppt-segment-container")
.on("click", node.segmentClick)
.on("mouseover", function (d) {
d3.select(this).select(".ppt-text-arc").classed("hover", true)
})
.on("mouseout", function (d) {
d3.select(this).select(".ppt-text-arc").classed("hover", false)
});
gLabel.append("path")
.attr("class", "ppt-hidden-arc")
.attr("id", function (d, i) {
var n = d3.select(this.parentNode.parentNode).datum();
return "arc_" + n.id + "_" + i;
})
.attr("d", function (relationship) {
var n = d3.select(this.parentNode.parentNode).datum();
//A regular expression that captures all in between the start of a string (denoted by ^)
//and the first capital letter L
var firstArcSection = /(^.+?)L/;
var singleArcSection = /(^.+?)M/;
var intermediateArc = {
startAngle: relationship.directionAngle - (Math.PI - 0.1),
endAngle: relationship.directionAngle + (Math.PI - 0.1)
};
var arcPath = d3.arc()
.innerRadius(node.getDonutInnerRadius(n))
.outerRadius(node.getDonutOuterRadius(n))(intermediateArc);
//The [1] gives back the expression between the () (thus not the L as well)
//which is exactly the arc statement
var res = firstArcSection.exec(arcPath);
var newArc = "";
if (res && res.length > 1) {
newArc = res[1];
} else {
newArc = singleArcSection.exec(arcPath)[1];
}
//Replace all the comma's so that IE can handle it -_-
//The g after the / is a modifier that "find all matches rather than stopping after the first match"
newArc = newArc.replace(/,/g, " ");
return newArc;
})
.style("fill", "none")
.style("stroke", "none");
gSegment.append("text")
.attr("text-anchor", "middle")
.attr("class", function (d) {
var n = d3.select(this.parentNode.parentNode).datum();
if (n.hasOwnProperty("count") && n.count === 0) {
return "ppt-text-arc disabled";
} else {
return "ppt-text-arc";
}
})
.attr("fill", function (d) {
var n = d3.select(this.parentNode.parentNode).datum();
return provider.link.getColor({
label: d.label,
type: graph.link.LinkTypes.SEGMENT,
source: n,
target: {label: d.target}
}, "segment", "fill");
})
.attr("dy", graph.link.TEXT_DY)
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", function (d, i) {
var n = d3.select(this.parentNode.parentNode.parentNode).datum();
return "#arc_" + n.id + "_" + i;
})
.text(function (d) {
var n = d3.select(this.parentNode.parentNode.parentNode).datum();
return provider.link.getTextValue({
source: n,
target: {label: d.target},
label: d.label,
type: graph.link.LinkTypes.SEGMENT
});
});
gSegment.append("path")
.attr("class", function (d) {
var n = d3.select(this.parentNode.parentNode).datum();
if (n.hasOwnProperty("count") && n.count === 0) {
return "ppt-segment disabled";
} else {
return "ppt-segment";
}
})
.attr("d", function (d) {
var n = d3.select(this.parentNode.parentNode).datum();
return d3.arc()
.innerRadius(node.getDonutInnerRadius(n))
.outerRadius(node.getDonutOuterRadius(n))(d)
})
.attr("fill", function (d) {
var n = d3.select(this.parentNode.parentNode).datum();
return provider.link.getColor({
label: d.label,
type: graph.link.LinkTypes.RELATION,
source: n,
target: {label: d.target}
}, "path", "fill");
})
.attr("stroke", function (d) {
var n = d3.select(this.parentNode.parentNode).datum();
return provider.link.getColor({
label: d.label,
type: graph.link.LinkTypes.RELATION,
source: n,
target: {label: d.target}
}, "path", "stroke");
})
;
};
/**
* Update the middle layer of nodes.
* TODO refactor node generation to allow future extensions (for example add plugin with new node types...)
*/
node.updateMiddlegroundElements = function () {
var middleG = graph.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-middleground");
middleG.attr("clip-path", function (n) {
return "url(#node-view" + n.id + ")";
});
// Clear all content in case node type has changed
middleG.selectAll("*").remove();
node.updateMiddlegroundElementsTooltip(middleG);
node.updateMiddlegroundElementsText(middleG.filter(function (d) {
return provider.node.getNodeDisplayType(d) === provider.node.DisplayTypes.TEXT;
}));
node.updateMiddlegroundElementsImage(middleG.filter(function (d) {
return provider.node.getNodeDisplayType(d) === provider.node.DisplayTypes.IMAGE;
}));
node.updateMiddlegroundElementsSymbol(middleG.filter(function (d) {
return provider.node.getNodeDisplayType(d) === provider.node.DisplayTypes.SYMBOL;
}));
node.updateMiddlegroundElementsSVG(middleG.filter(function (d) {
return provider.node.getNodeDisplayType(d) === provider.node.DisplayTypes.SVG;
}));
node.updateMiddlegroundElementsDisplayedText(middleG.filter(function (d) {
return provider.node.isTextDisplayed(d);
}));
};
node.updateMiddlegroundElementsTooltip = function (middleG) {
// Most browser will generate a tooltip if a title is specified for the SVG element
// TODO Introduce an SVG tooltip instead?
middleG.append("title")
.attr("class", function (n) {
return provider.node.getCSSClass(n, "title")
})
.text(function (d) {
return provider.node.getTextValue(d, node.NODE_TITLE_MAX_CHARS);
});
};
node.updateMiddlegroundElementsText = function (gMiddlegroundTextNodes) {
var circle = gMiddlegroundTextNodes.append("circle").attr("r", function (n) {
return provider.node.getSize(n);
});
// Set class according to node type
circle
.attr("class", function (n) {
return provider.node.getCSSClass(n, "circle")
})
.attr("fill", function (n) {
return provider.node.getColor(n, "circle", "fill");
})
.attr("stroke", function (n) {
return provider.node.getColor(n, "circle", "stroke");
});
};
node.updateMiddlegroundElementsImage = function (gMiddlegroundImageNodes) {
gMiddlegroundImageNodes.append("circle").attr("r", function (n) {
return provider.node.getSize(n);
})
.attr("class", function (n) {
return provider.node.getCSSClass(n, "image-background-circle")
});
gMiddlegroundImageNodes.append("image")
.attr("class", function (n) {
return provider.node.getCSSClass(n, "image")
})
.attr("width", function (d) {
return provider.node.getImageWidth(d);
})
.attr("height", function (d) {
return provider.node.getImageHeight(d);
})
// Center the image on node
.attr("transform", function (d) {
return "translate(" + (-provider.node.getImageWidth(d) / 2) + "," + (-provider.node.getImageHeight(d) / 2) + ")";
})
.attr("xlink:href", function (d) {
return provider.node.getImagePath(d);
});
};
node.updateMiddlegroundElementsSymbol = function (gMiddlegroundSymbolNodes) {
gMiddlegroundSymbolNodes.append("circle").attr("r", function (n) {
return provider.node.getSize(n);
})
.attr("class", function (n) {
return provider.node.getCSSClass(n, "symbol-background-circle")
})
.attr("fill", function (n) {
return provider.node.getColor(n, "circle", "fill");
})
.attr("stroke", function (n) {
return provider.node.getColor(n, "circle", "stroke");
});
gMiddlegroundSymbolNodes.append("use")
.attr("class", function (n) {
return provider.node.getCSSClass(n, "symbol")
})
.attr("width", function (d) {
return provider.node.getImageWidth(d);
})
.attr("height", function (d) {
return provider.node.getImageHeight(d);
})
// Center the image on node
.attr("transform", function (d) {
return "translate(" + (-provider.node.getImageWidth(d) / 2) + "," + (-provider.node.getImageHeight(d) / 2) + ")";
})
.attr("xlink:href", function (d) {
return provider.node.getImagePath(d);
})
.attr("fill", function (n) {
return provider.node.getColor(n, "circle", "fill");
})
.attr("stroke", function (n) {
return provider.node.getColor(n, "circle", "stroke");
});
};
node.updateMiddlegroundElementsSVG = function (gMiddlegroundSVGNodes) {
var SVGmiddleG = gMiddlegroundSVGNodes.append("g");
var circle = SVGmiddleG.append("circle").attr("r", function (n) {
return provider.node.getSize(n);
}).attr("class", "ppt-svg-node-background");
var svgMiddlePaths = SVGmiddleG.selectAll("path").data(function (d) {
return provider.node.getSVGPaths(d);
});
// Update nested data elements
svgMiddlePaths.exit().remove();
svgMiddlePaths.enter().append("path");
SVGmiddleG
.selectAll("path")
.attr("class", function (d) {
var n = d3.select(this.parentNode).datum();
return provider.node.getCSSClass(n, "path")
})
.each(function (d, i) {
for (var prop in d) {
if (d.hasOwnProperty(prop)) {
d3.select(this).attr(prop, d[prop]);
}
}
})
};
node.updateMiddlegroundElementsDisplayedText = function (middleG) {
var textDisplayed = middleG.filter(function (d) {
return provider.node.isTextDisplayed(d);
});
if (graph.USE_FIT_TEXT) {
fitTextRenderer.render(textDisplayed);
} else {
textRenderer.render(textDisplayed);
}
};
/**
* Updates the foreground elements
*/
node.updateForegroundElements = function () {
// Updates browse arrows status
// TODO ZZZ extract variable?
var gArrows = graph.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-foreground")
.selectAll(".ppt-node-foreground-g-arrows");
gArrows.classed("active", function (d) {
return d.valueExpanded && d.data && d.data.length > node.PAGE_SIZE;
});
gArrows.selectAll(".ppt-larrow").classed("enabled", function (d) {
return d.page > 1;
});
gArrows.selectAll(".ppt-rarrow").classed("enabled", function (d) {
if (d.data) {
var count = d.data.length;
return d.page * node.PAGE_SIZE < count;
} else {
return false;
}
});
// Update count box class depending on node type
var gForegrounds = graph.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-foreground");
gForegrounds.selectAll(".ppt-count-box").filter(function (d) {
return d.type !== node.NodeTypes.CHOOSE;
}).classed("root", true);
gForegrounds.selectAll(".ppt-count-box").filter(function (d) {
return d.type === node.NodeTypes.CHOOSE;
}).classed("value", true);
gForegrounds.selectAll(".ppt-count-box").classed("disabled", function (d) {
return d.count === 0;
});
if (!graph.DISABLE_COUNT) {
gForegrounds.selectAll(".ppt-count-text")
.text(function (d) {
if (d.count !== null) {
return d.count;
} else {
return "...";
}
})
.classed("disabled", function (d) {
return d.count === 0;
});
}
graph.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-foreground").filter(function (n) {
return n.isNegative === true;
}).selectAll(".ppt-g-node-ban")
.attr("transform", function (d) {
return "translate(" + (-provider.node.getSize(d)) + "," + (-provider.node.getSize(d)) + ") " +
"scale(" + ((provider.node.getSize(d) * 2) / 100) + ")"; // 100 is the size of the image drawn with the path
})
.attr("stroke-width", function (d) {
return (2 / ((provider.node.getSize(d) * 2) / 100)) + "px";
});
graph.svg.select("#" + node.gID).selectAll(".ppt-gnode").selectAll(".ppt-g-node-foreground").selectAll(".ppt-g-node-ban")
.classed("active", function (n) {
return n.isNegative === true;
});
};
node.segmentClick = function (event, d) {
event.preventDefault();
var n = d3.select(this.parentNode.parentNode).datum();
graph.ignoreCount = true;
graph.addRelationshipData(n, d, function (targetNode) {
graph.notifyListeners(graph.Events.GRAPH_NODE_RELATION_ADD, [
dataModel.links.filter(function (l) {
return l.target === targetNode;
})
]);
graph.ignoreCount = false;
graph.hasGraphChanged = true;
update();
});
};
/**
* Handle the mouse over event on nodes.
*/
node.mouseOverNode = function (event) {
event.preventDefault();
// TODO don't work on IE (nodes unstable) find another way to move node in foreground on mouse over?
// d3.select(this).moveToFront();
// tootip.div.style("display", "inline");
var hoveredNode = d3.select(this).data()[0];
if (queryviewer.isActive) {
// Hover the node in query
queryviewer.queryConstraintSpanElements.filter(function (d) {
return d.ref === hoveredNode;
}).classed("hover", true);
queryviewer.querySpanElements.filter(function (d) {
return d.ref === hoveredNode;
}).classed("hover", true);
}
if (cypherviewer.isActive) {
cypherviewer.querySpanElements.filter(function (d) {
return d.node === hoveredNode;
}).classed("hover", true);
}
};
/**
* Handle mouse out event on nodes.
*/
node.mouseOutNode = function (event) {
event.preventDefault();
// tootip.div.style("display", "none");
var hoveredNode = d3.select(this).data()[0];
if (queryviewer.isActive) {
// Remove hover class on node.
queryviewer.queryConstraintSpanElements.filter(function (d) {
return d.ref === hoveredNode;
}).classed("hover", false);
queryviewer.querySpanElements.filter(function (d) {
return d.ref === hoveredNode;
}).classed("hover", false);
}
if (cypherviewer.isActive) {
cypherviewer.querySpanElements.filter(function (d) {
return d.node === hoveredNode;
}).classed("hover", false);
}
};
/**
* Handle the click event on nodes.
*/
node.nodeClick = function (event) {
if (!event.defaultPrevented) { // To avoid click on drag end
var clickedNode = d3.select(this).data()[0]; // Clicked node data
logger.debug("nodeClick (" + clickedNode.label + ")");
if (clickedNode.type === node.NodeTypes.VALUE) {
node.valueNodeClick(clickedNode);
} else if (clickedNode.type === node.NodeTypes.CHOOSE || clickedNode.type === node.NodeTypes.ROOT) {
if (event.ctrlKey) {
if (clickedNode.type === node.NodeTypes.CHOOSE) {
clickedNode.isNegative = !clickedNode.hasOwnProperty("isNegative") || !clickedNode.isNegative;
node.collapseAllNode();
if (clickedNode.hasOwnProperty("value") && clickedNode.value.length > 0) {
} else {
if (clickedNode.isNegative) {
// Remove all related nodes
for (var i = dataModel.links.length - 1; i >= 0; i--) {
if (dataModel.links[i].source === clickedNode) {
node.removeNode(dataModel.links[i].target);
}
}
clickedNode.count = 0;
}
}
result.hasChanged = true;
graph.hasGraphChanged = true;
update();
} // negation not supported on root node
} else {
if (clickedNode.valueExpanded) {
node.collapseNode(clickedNode);
} else {
node.chooseNodeClick(clickedNode);
}
}
}
}
};
/**
* Remove all the value node directly linked to clicked node.
*
* @param clickedNode
*/
node.collapseNode = function (clickedNode) {
if (clickedNode.valueExpanded) { // node is collapsed only if it has been expanded first
logger.debug("collapseNode (" + clickedNode.label + ")");
graph.notifyListeners(graph.Events.GRAPH_NODE_VALUE_COLLAPSE, [clickedNode]);
var linksToRemove = dataModel.links.filter(function (l) {
return l.source === clickedNode && l.type === graph.link.LinkTypes.VALUE;
});
// Remove children nodes from model
linksToRemove.forEach(function (l) {
dataModel.nodes.splice(dataModel.nodes.indexOf(l.target), 1);
});
// Remove links from model
for (var i = dataModel.links.length - 1; i >= 0; i--) {
if (linksToRemove.indexOf(dataModel.links[i]) >= 0) {
dataModel.links.splice(i, 1);
}
}
// Node has been fixed when expanded so we unfix it back here.
if (clickedNode.type !== node.NodeTypes.ROOT) {
clickedNode.fixed = false;
clickedNode.fx = null;
clickedNode.fy = null;
}
// Parent node too if not root
if (clickedNode.parent && clickedNode.parent.type !== node.NodeTypes.ROOT) {
clickedNode.parent.fixed = false;
clickedNode.parent.fx = null;
clickedNode.parent.fy = null;
}
clickedNode.valueExpanded = false;
update();
} else {
logger.debug("collapseNode called on an unexpanded node");
}
};
/**
* Collapse all nodes with value expanded.
*
*/
node.collapseAllNode = function () {
dataModel.nodes.forEach(function (n) {
if ((n.type === node.NodeTypes.CHOOSE || n.type === node.NodeTypes.ROOT) && n.valueExpanded) {
node.collapseNode(n);
}
});
};
/**
* Function called on a value node click.
* In this case the value is added in the parent node and all the value nodes are collapsed.
*
* @param clickedNode
*/
node.valueNodeClick = function (clickedNode) {
logger.debug("valueNodeClick (" + clickedNode.label + ")");
graph.notifyListeners(graph.Events.GRAPH_NODE_ADD_VALUE, [clickedNode]);
if (clickedNode.parent.value === undefined) {
clickedNode.parent.value = [];
}
clickedNode.parent.value.push(clickedNode);
result.hasChanged = true;
graph.hasGraphChanged = true;
node.collapseNode(clickedNode.parent);
};
/**
* Function called on choose node click.
* In this case a query is executed to get all the possible value
* @param clickedNode
* TODO optimize with cached data?
*/
node.chooseNodeClick = function (clickedNode) {
logger.debug("chooseNodeClick (" + clickedNode.label + ") with waiting state set to " + node.chooseWaiting);
if (!node.chooseWaiting && !clickedNode.immutable && !(clickedNode.count === 0)) {
// Collapse all expanded nodes first
node.collapseAllNode();
// Set waiting state to true to avoid multiple call on slow query execution
node.chooseWaiting = true;
// Don't run query to get value if node isAutoLoadValue is set to true
if (clickedNode.data !== undefined && clickedNode.isAutoLoadValue) {
clickedNode.page = 1;
node.expandNode(clickedNode);
node.chooseWaiting = false;
} else {
logger.info("Values (" + clickedNode.label + ") ==>");
var nodeValueQuery = query.generateNodeValueQuery(clickedNode);
runner.run(
{
"statements": [
{
"statement": nodeValueQuery.statement,
"parameters": nodeValueQuery.parameters
}]
})
.then(function (results) {
logger.info("<== Values (" + clickedNode.label + ")");
var parsedData = runner.toObject(results);
var constraintAttr = provider.node.getConstraintAttribute(clickedNode.label);
clickedNode.data = parsedData[0].filter(function (dataToFilter) {
var keepData = true;
if (clickedNode.hasOwnProperty("value") && clickedNode.value.length > 0) {
clickedNode.value.forEach(function (value) {
if (value.attributes[constraintAttr] === dataToFilter[constraintAttr]) {
keepData = false;
}
})
}
return keepData;
});
clickedNode.page = 1;
node.expandNode(clickedNode);
node.chooseWaiting = false;
})
.catch(function (error) {
node.chooseWaiting = false;
logger.error(error);
});
}
}
};
/**
* Add in all expanded choose nodes the value containing the specified value for the given attribute.
* And remove it from the nodes data.
*
* @param attribute
* @param value
*/
node.addExpandedValue = function (attribute, value) {
var isAnyChangeDone = false;
// For each expanded nodes
for (var i = dataModel.nodes.length - 1; i >= 0; i--) {
if (dataModel.nodes[i].valueExpanded) {
// Look in node data if value can be found in reverse order to be able to remove value without effect on iteration index
for (var j = dataModel.nodes[i].data.length - 1; j >= 0; j--) {
if (dataModel.nodes[i].data[j][attribute] === value) {
isAnyChangeDone = true;
// Create field value if needed
if (!dataModel.nodes[i].hasOwnProperty("value")) {
dataModel.nodes[i].value = [];
}
// Add value
dataModel.nodes[i].value.push({
attributes: dataModel.nodes[i].data[j]
});
// Remove data added in value
dataModel.nodes[i].data.splice(j, 1);
}
}
// Refresh node
node.collapseNode(dataModel.nodes[i]);
node.expandNode(dataModel.nodes[i]);
}
}
if (isAnyChangeDone) {
result.hasChanged = true;
graph.hasGraphChanged = true;
update();
}
};
/**
* Get all nodes that contains a value.
*
* @param label If set return only node of this label.
* @return {Array} Array of nodes containing at least one value.
*/
node.getContainingValue = function (label) {
var nodesWithValue = [];
var links = dataModel.links, nodes = dataModel.nodes;
if (nodes.length > 0) {
var rootNode = nodes[0];
// Add root value
if (rootNode.value !== undefined && rootNode.value.length > 0) {
if (label === undefined || label === rootNode.label) {
nodesWithValue.push(rootNode);
}
}
links.forEach(function (l) {
var targetNode = l.target;
if (l.type === graph.link.LinkTypes.RELATION && targetNode.value !== undefined && targetNode.value.length > 0) {
if (label === undefined || label === targetNode.label) {
nodesWithValue.push(targetNode);
}
}
});
}
return nodesWithValue;
};
/**
* Add value in all CHOOSE nodes with specified label.
*
* @param label nodes where to insert
* @param value
*/
node.addValueForLabel = function (label, value) {
var isAnyChangeDone = false;
// Find choose node with label
for (var i = dataModel.nodes.length - 1; i >= 0; i--) {
if (dataModel.nodes[i].type === node.NodeTypes.CHOOSE && dataModel.nodes[i].label === label) {
// Create field value if needed
if (!dataModel.nodes[i].hasOwnProperty("value")) {
dataModel.nodes[i].value = [];
}
// check if value already exists
var isValueFound = false;
var constraintAttr = provider.node.getConstraintAttribute(label);
dataModel.nodes[i].value.forEach(function (val) {
if (val.attributes.hasOwnProperty(constraintAttr) && val.attributes[constraintAttr] === value.attributes[constraintAttr]) {
isValueFound = true;
}
});
if (!isValueFound) {
// Add value
dataModel.nodes[i].value.push(value);
isAnyChangeDone = true;
}
}
}
return isAnyChangeDone;
};
/**
* Add a value in a node with the given id and the value of the first attribute if found in its data.
*
* @param nodeIds a list of node ids where to add the value.
* @param displayAttributeValue the value to find in data and to add if found
*/
node.addValue = function (nodeIds, displayAttributeValue) {
var isAnyChangeDone = false;
// Find choose node with label
for (var i = 0; i < dataModel.nodes.length; i++) {
var n = dataModel.nodes[i];
if (nodeIds.indexOf(n.id) >= 0) {
// Create field value in node if needed
if (!n.hasOwnProperty("value")) {
n.value = [];
}
var displayAttr = provider.node.getReturnAttributes(n.label)[0];
// Find data for this node and add value
n.data.forEach(function (d) {
if (d.hasOwnProperty(displayAttr) && d[displayAttr] === displayAttributeValue) {
isAnyChangeDone = true;
n.value.push({attributes: d})
}
});
}
}
if (isAnyChangeDone) {
result.hasChanged = true;
graph.hasGraphChanged = true;
update();
}
};
/**
* Remove a value from a node.
* If the value is not found nothing is done.
*
* @param n
* @param value
*/
node.removeValue = function (n, value) {
var isAnyChangeDone = false;
node.collapseNode(n);
for (var j = n.value.length - 1; j >= 0; j--) {
if (n.value[j] === value) {
n.value.splice(j, 1);
isAnyChangeDone = true;
}
}
return isAnyChangeDone;
};
node.removeValues = function (n) {
var isAnyChangeDone = false;
node.collapseNode(n);
if (n.value !== undefined && n.value.length > 0) {
n.value.length = 0;
isAnyChangeDone = true;
}
return isAnyChangeDone
};
/**
* Get the value in the provided nodeId for a specific value id.
*
* @param nodeId
* @param constraintAttributeValue
*/
node.getValue = function (nodeId, constraintAttributeValue) {
for (var i = 0; i < dataModel.nodes.length; i++) {
var n = dataModel.nodes[i];
if (n.id === nodeId) {
var constraintAttribute = provider.node.getConstraintAttribute(n.label);
for (var j = n.value.length - 1; j >= 0; j--) {
if (n.value[j].attributes[constraintAttribute] === constraintAttributeValue) {
return n.value[j]
}
}
}
}
};
/**
* Remove in all expanded nodes the value containing the specified value for the given attribute.
* And move it back to nodes data.
*
* @param attribute
* @param value
*/
node.removeExpandedValue = function (attribute, value) {
var isAnyChangeDone = false;
// For each expanded nodes in reverse order as some values can be removed
for (var i = dataModel.nodes.length - 1; i >= 0; i--) {
if (dataModel.nodes[i].valueExpanded) {
var removedValues = [];
// Remove values
for (var j = dataModel.nodes[i].value.length - 1; j >= 0; j--) {
if (dataModel.nodes[i].value[j].attributes[attribute] === value) {
isAnyChangeDone = true;
removedValues = removedValues.concat(dataModel.nodes[i].value.splice(j, 1));
}
}
//And add them back in data
for (var k = 0; k < removedValues.length; k++) {
dataModel.nodes[i].data.push(removedValues[k].attributes);
}
// Refresh node
node.collapseNode(dataModel.nodes[i]);
node.expandNode(dataModel.nodes[i]);
}
}
if (isAnyChangeDone) {
result.hasChanged = true;
graph.hasGraphChanged = true;
update();
}
};
/**
* Return all nodes with isAutoLoadValue property set to true.
*/
node.getAutoLoadValueNodes = function () {
return dataModel.nodes
.filter(function (d) {
return d.hasOwnProperty("isAutoLoadValue") && d.isAutoLoadValue === true && !(d.isNegative === true);
});
};
/**
* Add a list of related value if not already found in node.
* A value is defined with the following structure
* {
* id,
* rel,
* label
* }
*
* @param n
* @param values
* @param isNegative
*/
node.addRelatedValues = function (n, values, isNegative) {
var valuesToAdd = node.filterExistingValues(n, values);
if (valuesToAdd.length <= 0) {
return;
}
var statements = [];
valuesToAdd.forEach(function (v) {
var constraintAttr = provider.node.getConstraintAttribute(v.label);
var statement = "MATCH ";
if (constraintAttr === query.NEO4J_INTERNAL_ID) {
statement += "(v:`" + v.label + "`) WHERE (ID(v) = $p)";
} else {
statement += "(v:`" + v.label + "`) WHERE (v." + constraintAttr + " = $p)";
}
var resultAttributes = provider.node.getReturnAttributes(v.label);
var sep = "";
statement += " RETURN DISTINCT \"" + v.rel + "\" AS rel, \"" + v.label + "\" AS label, {" + resultAttributes.reduce(function (a, attr) {
a += sep + attr + ":v." + attr;
sep = ", ";
return a
}, "") + "} AS value LIMIT 1";
statements.push(
{
"statement": statement,
"parameters": {p: v.id},
"resultDataContents": ["row"]
}
)
});
logger.info("addRelatedValues ==>");
runner.run(
{
"statements": statements
})
.then(function (results) {
logger.info("<== addRelatedValues");
var parsedData = runner.toObject(results);
var count = 0;
parsedData.forEach(function (data) {
if (data.length > 0) {
var dataLabel = data[0].label;
var dataValue = data[0].value;
var dataRel = data[0].rel;
var value = {
"id": dataModel.generateId(),
"parent": n,
"attributes": dataValue,
"type": node.NodeTypes.VALUE,
"label": dataLabel
};
graph.ignoreCount = true;
var nodeRelationships = n.relationships;
var nodeRels = nodeRelationships.filter(function (r) {
return r.label === dataRel && r.target === dataLabel
});
var nodeRel = {label: dataRel, target: dataLabel};
if (nodeRels.length > 0) {
nodeRel = nodeRels[0];
}
graph.addRelationshipData(n, nodeRel, function () {
count++;
if (count === parsedData.length) {
graph.ignoreCount = false;
graph.hasGraphChanged = true;
result.hasChanged = true;
update();
}
}, [value], isNegative);
}
});
})
.catch(function (error) {
logger.error(error);
});
};
/**
* Add a list of related value prefixed by a path of nodes.
* A value is defined with the following structure
* {
* id,
* rel,
* label
* }
*
* @param n
* @param relPath
* @param values
* @param isNegative
*/
node.addRelatedBranch = function (n, relPath, values, isNegative) {
if (relPath.length > 0) {
var rel = relPath[0];
relPath = relPath.slice(1);
var relationships = n.relationships.filter(function (r) {
return r.label === rel.type && r.target === rel.target;
});
if (relationships.length > 0) {
graph.addRelationshipData(n, relationships[0], function (createdNode) {
node.addRelatedBranch(createdNode, relPath, values, isNegative);
});
}
} else {
node.addRelatedValues(n, values, isNegative)
}
};
/**
* A value is defined with the following structure
* {
* id,
* rel,
* label
* }
*
* @param n
* @param values
*/
node.filterExistingValues = function (n, values) {
var notFoundValues = [];
var possibleValueNodes = dataModel.nodes.filter(function (n) {
return n.parent === n && n.hasOwnProperty("value") && n.value.length > 0;
});
values.forEach(function (v) {
var found = false;
var constraintAttr = provider.node.getConstraintAttribute(v.label);
possibleValueNodes.forEach(function (n) {
if (n.label === v.label) {
n.value.forEach(function (nv) {
if (nv.attributes[constraintAttr] === v.id) {
found = true;
}
});
}
});
if (!found) {
notFoundValues.push(v)
}
});
return notFoundValues;
};
/**
* Function called to expand a node containing values.
* This function will create the value nodes with the clicked node internal data.
* Only nodes corresponding to the current page index will be generated.
*
* @param clickedNode
*/
node.expandNode = function (clickedNode) {
graph.notifyListeners(graph.Events.GRAPH_NODE_VALUE_EXPAND, [clickedNode]);
// Get subset of node corresponding to the current node page and page size
var lIndex = clickedNode.page * node.PAGE_SIZE;
var sIndex = lIndex - node.PAGE_SIZE;
var dataToAdd = clickedNode.data.slice(sIndex, lIndex);
var parentAngle = graph.computeParentAngle(clickedNode);
// Then each node are created and dispatched around the clicked node using computed coordinates.
var i = 1;
dataToAdd.forEach(function (d) {
var angleDeg;
if (clickedNode.parent) {
angleDeg = (((360 / (dataToAdd.length + 1)) * i));
} else {
angleDeg = (((360 / (dataToAdd.length)) * i));
}
var nx = clickedNode.x + (100 * Math.cos((angleDeg * (Math.PI / 180)) - parentAngle)),
ny = clickedNode.y + (100 * Math.sin((angleDeg * (Math.PI / 180)) - parentAngle));
var n = {
"id": dataModel.generateId(),
"parent": clickedNode,
"attributes": d,
"type": node.NodeTypes.VALUE,
"label": clickedNode.label,
"count": d.count,
"x": nx,
"y": ny,
"internalID": d[query.NEO4J_INTERNAL_ID.queryInternalName]
};
dataModel.nodes.push(n);
dataModel.links.push(
{
id: "l" + dataModel.generateId(),
source: clickedNode,
target: n,
type: graph.link.LinkTypes.VALUE
}
);
i++;
});
// Pin clicked node and its parent to avoid the graph to move for selection, only new value nodes will blossom around the clicked node.
clickedNode.fixed = true;
clickedNode.fx = clickedNode.x;
clickedNode.fy = clickedNode.y;
if (clickedNode.parent && clickedNode.parent.type !== node.NodeTypes.ROOT) {
clickedNode.parent.fixed = true;
clickedNode.parent.fx = clickedNode.parent.x;
clickedNode.parent.fy = clickedNode.parent.y;
}
// Change node state
clickedNode.valueExpanded = true;
update();
};
/**
* Fetches the list of relationships of a node and store them in the relationships property.
*
* @param n the node to fetch the relationships.
* @param callback
* @param directionAngle
*/
node.loadRelationshipData = function (n, callback, directionAngle) {
var schema = provider.node.getSchema(n.label);
if (schema !== undefined) {
if (schema.hasOwnProperty("rel") && schema.rel.length > 0) {
callback(node.pie.startAngle(directionAngle - Math.PI).endAngle(directionAngle + Math.PI)(schema.rel).map(function (d) {
var data = {
id: d.data.label + d.data.target.label,
label: d.data.label,
target: d.data.target.label,
count: 0,
startAngle: d.startAngle,
endAngle: d.endAngle,
directionAngle: (d.startAngle + d.endAngle) / 2
};
if (d.data.isReverse === true) {
data.isReverse = true;
}
return data;
}));
} else {
callback([]);
}
} else {
var nodeRelationQuery = query.generateNodeRelationQuery(n);
logger.info("Relations (" + n.label + ") ==>");
runner.run(
{
"statements": [
{
"statement": nodeRelationQuery.statement,
"parameters": nodeRelationQuery.parameters
}]
})
.then(function (results) {
logger.info("<== Relations (" + n.label + ")");
var parsedData = runner.toObject(results);
// Filter data to eventually remove relations if a filter has been defined in config.
var filteredData = parsedData[0].filter(function (d) {
return query.filterRelation(d);
});
filteredData = node.pie.startAngle(directionAngle - Math.PI).endAngle(directionAngle + Math.PI)(filteredData).map(function (d) {
return {
id: d.data.label + d.data.target,
label: d.data.label,
target: d.data.target,
count: d.data.count.toString(),
startAngle: d.startAngle,
endAngle: d.endAngle,
directionAngle: (d.startAngle + d.endAngle) / 2
}
});
callback(filteredData);
})
.catch(function (error) {
logger.error(error);
callback([]);
});
}
};
/**
* Expands all the relationships available in node.
*
* @param n
* @param callback
*/
node.expandRelationships = function (n, callback) {
var callbackCount = 0;
if (n.hasOwnProperty("relationships") && n.relationships.length > 0) {
for (var i = 0; i < n.relationships.length; i++) {
graph.addRelationshipData(n, n.relationships[i], function () {
callbackCount++;
if (callbackCount === n.relationships.length) {
callback();
}
})
}
} else {
callback();
}
};
/**
* Remove a node and its relationships (recursively) from the graph.
*
* @param n the node to remove.
*/
node.removeNode = function (n) {
var willChangeResults = n.hasOwnProperty("value") && n.value.length > 0;
var linksToRemove = dataModel.links.filter(function (l) {
return l.source === n;
});
// Remove children nodes from model
linksToRemove.forEach(function (l) {
var rc = node.removeNode(l.target);
willChangeResults = willChangeResults || rc;
});
// Remove links to nodes from model
for (var i = dataModel.links.length - 1; i >= 0; i--) {
if (dataModel.links[i].target === n) {
dataModel.links.splice(i, 1);
}
}
dataModel.nodes.splice(dataModel.nodes.indexOf(n), 1);
return willChangeResults;
};
/**
* Remove empty branches containing a node.
*
* @param n the node to remove.
* @return true if node have been removed
*/
node.removeEmptyBranches = function (n) {
var hasValues = n.hasOwnProperty("value") && n.value.length > 0;
var childrenLinks = dataModel.links.filter(function (l) {
return l.source === n;
});
childrenLinks.forEach(function (l) {
var hasRemainingNodes = !node.removeEmptyBranches(l.target);
hasValues = hasValues || hasRemainingNodes;
});
if (!hasValues) {
// Remove links to node from model
for (var i = dataModel.links.length - 1; i >= 0; i--) {
if (dataModel.links[i].target === n) {
dataModel.links.splice(i, 1);
}
}
dataModel.nodes.splice(dataModel.nodes.indexOf(n), 1);
}
return !hasValues;
};
/**
* Get in the parent nodes the closest one to the root.
*
* @param n the node to start from.
* @return {*} the trunk node or the node in parameters if not found.
*/
node.getTrunkNode = function (n) {
for (var i = 0; i < dataModel.links.length; i++) {
var l = dataModel.links[i];
if (l.target === n) {
if (l.source !== graph.getRootNode()) {
return node.getTrunkNode(l.source);
}
}
}
return n;
};
/**
* Function to add on node event to clear the selection.
* Call to this function on a node will remove the selected value and trigger a graph update.
*/
node.clearSelection = function (event) {
// Prevent default event like right click opening menu.
event.preventDefault();
// Get clicked node.
var clickedNode = d3.select(this).data()[0];
// Collapse all expanded choose nodes first
node.collapseAllNode();
if (clickedNode.value !== undefined && clickedNode.value.length > 0 && !clickedNode.immutable) {
// Remove last value of chosen node
clickedNode.value.pop();
if (clickedNode.isNegative === true) {
if (clickedNode.value.length === 0) {
// Remove all related nodes
for (var i = dataModel.links.length - 1; i >= 0; i--) {
if (dataModel.links[i].source === clickedNode) {
node.removeNode(dataModel.links[i].target);
}
}
clickedNode.count = 0;
}
}
result.hasChanged = true;
graph.hasGraphChanged = true;
update();
}
};
export default node;