bschmitty bschmitty - 3 months ago 42
Javascript Question

Dynamically Centering Filtered Node within SVG Element in D3

I am working to add filter functionality to my d3 graph. When the user searches for a specific node based on label or id, I want to re-render the graph and show the entire graph again but I want the filtered node to sit in the center of the svg element.

enter image description here

here is what I have the helped it to be centered:

// I get the width and height of the SVG element:
var svgWidth = parseInt(svg.style("width").replace(/px/, ""), 10);
var svgHeight = parseInt(svg.style("height").replace(/px/, ""), 10);

// I get the center of the svg:
var centerX = svgWidth / 2;
var centerY = svgHeight / 2;

_.forEach(nodes, function(e) {
// get the full node (with x and y coordinates) based on the id
var nodeObject = g.node(nodeId);

// I look for matches between the nodeId or label and search word
if (searchInput) {
if (nodeObject.id === parseInt(searchInput, 10) || nodeObject.label.toUpperCase().indexOf(searchInput.toUpperCase()) > -1) {
searchedNodes.push(nodeObject);
console.log(searchedNodes);
}
}
}

// after looping through all the nodes rendered
if (searchedNodes.length > 0) {
//var width = searchedNodes[0].elem.getBBox().width;
//var height = searchedNodes[0].elem.getBBox().height;
ctrl.selectedNode = searchedNodes[0];
var offsetX = centerX - searchedNodes[0].x;
var offsetY = centerY - searchedNodes[0].y;
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")" + "scale(" + 3 + ")");

// this line here is incorrect syntax and breaks the build, essentially stopping the script from running
// the graph renders correctly when this line is here
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")").scale(2).event;
}


This is what the graph looks like with the line above that breaks the script included.

enter image description here

When I removed that line, it doesn't center, almost looking like over-renders the graph. Obviously I will need to remove the line of code above that is incorrect but does anybody no why the graph doesn't render correctly in this case?:

enter image description here

// get the user input and re-render the graph
elem.find(".search").bind("keyup", function (e:any) {
var searchInput;
if (e["keyCode"] === 13) {
searchedNodes = [];
searchInput = scope["searchInput"];
currentFilteredNode = null;
enterKeyPressed = true;
renderGraph(searchInput);
}

if (e["keyCode"] === 8) {
searchedNodes = [];
searchInput = scope["searchInput"];
currentFilteredNode = null;
renderGraph(searchInput);
}
});

// if there is searchInput and at least one matching node sort the nodes
// by id and then select and center the first matching one
if (searchInput && searchedNodes.length > 0) {
searchedNodes.sort(function (node1:any, node2:any) {
return node1.id - node2.id;
});

// make sure the noResultsMessage does not get shown on the screen if there are matching results
scope.$apply(function() {
scope["noResultsMessage"] = false;
});

ctrl.selectedNode = searchedNodes[0];
offsetX = centerX - searchedNodes[0].x;
offsetY = centerY - searchedNodes[0].y;
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")" + "scale(" + 3 + ")");
}

// the only other zoom and this runs just on page load
zoom = d3.behavior.zoom();

zoom.on("zoom", function() {
svgGroup.attr("transform", "translate(" + (<any>d3.event).translate + ")" + "scale(" + (<any>d3.event).scale + ")");

// this scales the graph - it runs on page load and whenever the user enters a search input, which re-renders the whole graph
var scaleGraph = function(useAnimation:any) {
var graphWidth = g.graph().width + 4;
var graphHeight = g.graph().height + 4;
var width = parseInt(svg.style("width").replace(/px/, ""), 10);
var height = parseInt(svg.style("height").replace(/px/, ""), 10);
var zoomScale = originalZoomScale;
// Zoom and scale to fit
if (ctrl.autoResizeGraph === "disabled") {
zoomScale = 1;
} else {
// always scale to canvas if set to fill or if auto (when larger than canvas)
if (ctrl.autoResizeGraph === "fill" || (graphWidth > width || graphHeight > height)) {
zoomScale = Math.min(width / graphWidth, height / graphHeight);
}
}

var translate;

if (direction.toUpperCase() === "TB") {
// Center horizontal + align top (offset 1px)
translate = [(width / 2) - ((graphWidth * zoomScale) / 2) + 2, 1];
} else if (direction.toUpperCase() === "BT") {
// Center horizontal + align top (offset 1px)
translate = [(width / 2) - ((graphWidth * zoomScale) / 4) + 2, 1];
} else if (direction.toUpperCase() === "LR") {
// Center vertical (offset 1px)
translate = [1, (height / 2) - ((graphHeight * zoomScale) / 2)];
} else if (direction.toUpperCase() === "RL") {
// Center vertical (offset 1px)
translate = [1, (height / 2) - ((graphHeight * zoomScale) / 4)];
} else {
// Center horizontal and vertical
translate = [(width / 2) - ((graphWidth * zoomScale) / 2), (height / 2) - ((graphHeight * zoomScale) / 2)];
}

zoom.center([width / 2, height / 2]);
zoom.size([width, height]);

zoom.translate(translate);
zoom.scale(zoomScale);

// If rendering the first time, then don't use animation
zoom.event(useAnimation ? svg.transition().duration(500) : svg);
};


CODE FOR FILTERING THE NODES:

// move to the left of the searchedNodes array when the left arrow is clicked
scope["filterNodesLeft"] = function () {
filterNodesIndex--;
if (filterNodesIndex < 0) {
filterNodesIndex = searchedNodes.length - 1;
}
currentFilteredNode = searchedNodes[filterNodesIndex];
runScaleGraph = true;
number = 1;
renderGraph();
};

// move to the right of the searchNodes array when the right arrow is clicked
scope["filterNodesRight"] = function () {
filterNodesIndex++;
if (filterNodesIndex > searchedNodes.length - 1) {
filterNodesIndex = 0;
}
currentFilteredNode = searchedNodes[filterNodesIndex];
runScaleGraph = true;
number = 1;
renderGraph();
};

// get the current filteredNode in the searchNodes array and center it
// when the graph is re-rendered
if (currentFilteredNode) {
ctrl.selectedNode = currentFilteredNode;
offsetX = centerX - currentFilteredNode.x;
offsetY = centerY - currentFilteredNode.y;
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")");
runScaleGraph = false;
}

Answer

Here's how I solved it:

             // zoom in on the searched or filtered node
              function zoomOnNode (node:any) {

                // get the width and height of the svg
                var svgWidth = parseInt(svg.style("width").replace(/px/, ""), 10);
                var svgHeight = parseInt(svg.style("height").replace(/px/, ""), 10);

                // loop through all the rendered nodes (these nodes have x and y coordinates)
                for (var i = 0; i < renderedNodes.length; i++) {

                    // if the first matching node passed into the function
                    // and the renderedNode's id match get the 
                    // x and y coordinates from that rendered node and use it to calculate the svg transition
                    if (node.id === renderedNodes[i].id) {
                            var translate = [svgWidth / 2 -  renderedNodes[i].x, svgHeight / 2 - renderedNodes[i].y];
                            var scale = 1;
                            svg.transition().duration(750).call(zoom.translate(translate).scale(scale).event);
                    }
                }
            }

     // listen for the enter key press, get all matching nodes and pass in the first matching node in the array to the zoomOnNode function
     elem.find(".search").bind("keyup", function (e:any) {
                var searchInput;
                if (e["keyCode"] === 13) {
                    searchedNodes = [];
                    searchInput = scope["searchInput"];
                    enterKeyPressed = true;
                    if (searchInput) {
                        // recursively get all matching nodes based on search input
                        getMatchingNodes(ctrl.nodes, searchInput);

                        scope.$apply(function() {
                            // show the toggle icons if searchedNodes.length is greater then 1
                            scope["matchingNodes"] = searchedNodes.length;
                            scope["noResultsMessage"] = false;

                            if (searchedNodes.length > 0) {
                                var firstNode = searchedNodes[0];
                                ctrl.selectedNode = firstNode;
                                zoomOnNode(firstNode);
                            } else if (searchedNodes.length === 0) {
                                    ctrl.selectedNode = null;
                                    // add the noResultsMessage to the screen
                                    scope["noResultsMessage"] = true;
                                }
                            });
                        }
                }
          } 
Comments