UNPKG

popoto

Version:

Graph based search interface for Neo4j database.

1,499 lines (1,268 loc) 62 kB
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":