function createV4SelectableForceDirectedGraph(svg, graph) { // if both d3v3 and d3v4 are loaded, we'll assume // that d3v4 is called d3v4, otherwise we'll assume // that d3v4 is the default (d3) if (typeof d3v4 == 'undefined') d3v4 = d3; var width = +svg.attr("width"), height = +svg.attr("height"); let parentWidth = d3v4.select('svg').node().parentNode.clientWidth; let parentHeight = d3v4.select('svg').node().parentNode.clientHeight; var svg = d3v4.select('svg') .attr('width', parentWidth) .attr('height', parentHeight) // remove any previous graphs svg.selectAll('.g-main').remove(); var gMain = svg.append('g') .classed('g-main', true); var rect = gMain.append('rect') .attr('width', parentWidth) .attr('height', parentHeight) .style('fill', 'white') var gDraw = gMain.append('g') var zoom = d3v4.zoom() .on('zoom', zoomed) gMain.call(zoom); function zoomed() { gDraw.attr('transform', d3v4.event.transform); } var color = d3v4.scaleOrdinal(d3v4.schemeCategory20); if (! ("links" in graph)) { console.log("Graph is missing links"); return; } var nodes = {}; var i; for (i = 0; i < graph.nodes.length; i++) { nodes[graph.nodes[i].id] = graph.nodes[i]; graph.nodes[i].weight = 1.01; } // the brush needs to go before the nodes so that it doesn't // get called when the mouse is over a node var gBrushHolder = gDraw.append('g'); var gBrush = null; // add color gradient to edges function getGradID(d){return "linkGrad-" + d.source + "-"+ d.target;} var defs = d3v4.select("svg").append("defs"); var grads = defs.selectAll("linearGradient") .data(graph.links); var lingrads = grads.enter().append("linearGradient"); lingrads.attr("id", getGradID) .attr("gradientUnits", "userSpaceOnUse") //source = red lingrads.append("stop") .attr("offset", "0%") .attr("stop-color", "#FF0000") //target = green lingrads.append("stop") .attr("offset", "100%") .attr("stop-color", "#39ff14") var link = gDraw.append("g") .attr("class", "links") .selectAll("line") .data(graph.links) .enter().append("line") .attr("stroke-width", function(d) { return Math.sqrt(d.value); }); link.attr("source", function(d){return d.source}) .attr("target", function(d){return d.target}) .style("stroke", function(d){ return "url(#" + getGradID(d) + ")"; }) .attr("class", function(d) { var id1 = d.source.toString(); var id2 = d.target.toString(); if(d.bi_directional){ return "edge link-bi " + id1 + " " + id2; }else{ return "edge link " + id1 + " " + id2; } }) var node = gDraw.append("g") .attr("class", "nodes") .selectAll("circle") .data(graph.nodes) .enter().append("circle") .attr("r", 4) .attr("fill", function(d) { if ('color' in d) return d.color; else return "#626262"; //return color(d.group); }) .call(d3v4.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended)); node.attr("id", function(d){return d.id}) .attr("class", "node") // initialise force graph var simulation = d3v4.forceSimulation() .force("link", d3v4.forceLink() .id(function(d) { return d.id; }) .distance(function(d) { return 30; //var dist = 20 / d.value; //console.log('dist:', dist); return dist; }) ) .force("charge", d3v4.forceManyBody()) .force("center", d3v4.forceCenter(parentWidth / 2, parentHeight / 2)) .force("x", d3v4.forceX(parentWidth/2)) .force("y", d3v4.forceY(parentHeight/2)); simulation .nodes(graph.nodes) .on("tick", ticked) simulation.force("link") .links(graph.links); function ticked() { // update node and line positions at every step of // the force simulation link.attr("x1", function(d) { return d.source.x; }) .attr("y1", function(d) { return d.source.y; }) .attr("x2", function(d) { return d.target.x; }) .attr("y2", function(d) { return d.target.y; }); node.attr("cx", function(d) { return d.x; }) .attr("cy", function(d) { return d.y; }); lingrads.attr("x1", function(d){ return d.source.x }) .attr("y1", function(d){ return d.source.y; }) .attr("x2", function(d){ return d.target.x; }) .attr("y2", function(d){ return d.target.y; }); } var brushMode = false; var brushing = false; var brush = d3v4.brush() .on("start", brushstarted) .on("brush", brushed) .on("end", brushended); function brushstarted() { // keep track of whether we're actively brushing so that we // don't remove the brush on keyup in the middle of a selection brushing = true; node.each(function(d) { d.previouslySelected = shiftKey && d.selected; }); } rect.on('click', () => { node.each(function(d) { d.selected = false; d.previouslySelected = false; }); node.classed("selected", false); }); function brushed() { if (!d3v4.event.sourceEvent) return; if (!d3v4.event.selection) return; var extent = d3v4.event.selection; node.classed("selected", function(d) { return d.selected = d.previouslySelected ^ (extent[0][0] <= d.x && d.x < extent[1][0] && extent[0][1] <= d.y && d.y < extent[1][1]); }); } function brushended() { if (!d3v4.event.sourceEvent) return; if (!d3v4.event.selection) return; if (!gBrush) return; gBrush.call(brush.move, null); if (!brushMode) { // the shift key has been release before we ended our brushing gBrush.remove(); gBrush = null; } brushing = false; } d3v4.select('body').on('keydown', keydown); d3v4.select('body').on('keyup', keyup); var shiftKey; function keydown() { shiftKey = d3v4.event.shiftKey; if (shiftKey) { // if we already have a brush, don't do anything if (gBrush) return; brushMode = true; if (!gBrush) { gBrush = gBrushHolder.append('g'); gBrush.call(brush); } } } function keyup() { shiftKey = false; brushMode = false; if (!gBrush) return; if (!brushing) { // only remove the brush if we're not actively brushing // otherwise it'll be removed when the brushing ends gBrush.remove(); gBrush = null; } } function dragstarted(d) { if (!d3v4.event.active) simulation.alphaTarget(0.9).restart(); if (!d.selected && !shiftKey) { // if this node isn't selected, then we have to unselect every other node node.classed("selected", function(p) { return p.selected = p.previouslySelected = false; }); } d3v4.select(this).classed("selected", function(p) { d.previouslySelected = d.selected; return d.selected = true; }); node.filter(function(d) { return d.selected; }) .each(function(d) { //d.fixed |= 2; d.fx = d.x; d.fy = d.y; }) } function dragged(d) { //d.fx = d3v4.event.x; //d.fy = d3v4.event.y; node.filter(function(d) { return d.selected; }) .each(function(d) { d.fx += d3v4.event.dx; d.fy += d3v4.event.dy; }) } function dragended(d) { if (!d3v4.event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; node.filter(function(d) { return d.selected; }) .each(function(d) { //d.fixed &= ~6; d.fx = null; d.fy = null; }) } /*var texts = ['Use the scroll wheel to zoom', 'Hold the shift key to select nodes'] svg.selectAll('text') .data(texts) .enter() .append('text') .attr('x', 900) .attr('y', function(d,i) { return 470 + i * 18; }) .text(function(d) { return d; }); */ //change name on title to node clicked // Define the div for the tooltip var div = d3.select("body").append("div") .attr("class", "tooltip") .style("opacity", 0); node.on("mouseover", function(d) { div.transition() .duration(200) .style("opacity", .9); div .html(d.name) .style("left", (d3.event.pageX) + "px") .style("top", (d3.event.pageY - 28) + "px"); }) node.on("mouseout", function(d) { div.transition() .duration(500) .style("opacity", 0); }) // only show edges related to node on click allEdges = $('.edge'); allNodes = $('.node') node.on("click", function(d) { var id = d.id.toString(); allEdges.removeClass('hidden'); allNodes.removeClass('hidden'); nonRelevantEdges = $('.edge:not(.' + id + ')'); nonRelevantEdges.toggleClass('hidden'); relevantNodes = ($('.'+id).map(function(){return $(this).attr("source");}).get()).concat($('.'+id).map(function(){return $(this).attr("target");}).get()); relevantNodes[0] = '#' + relevantNodes[0]; nodeIdList = relevantNodes.join(", #"); allNodes.toggleClass('hidden') $(nodeIdList).removeClass('hidden'); }) // show all edges again when clicking on white space $('rect').on("click", function(d) { allEdges.removeClass('hidden'); allNodes.removeClass('hidden'); }) return graph; };