Built with blockbuilder.org
forked from anonymous's block: Multiple Dropdown Filters on one Force-Directed Graph
license: mit |
Built with blockbuilder.org
forked from anonymous's block: Multiple Dropdown Filters on one Force-Directed Graph
d3.functor = function functor(v) { | |
return typeof v === "function" ? v : function() { | |
return v; | |
}; | |
}; | |
d3.tip = function() { | |
var direction = d3_tip_direction, | |
offset = d3_tip_offset, | |
html = d3_tip_html, | |
node = initNode(), | |
svg = null, | |
point = null, | |
target = null | |
function tip(vis) { | |
svg = getSVGNode(vis) | |
point = svg.createSVGPoint() | |
document.body.appendChild(node) | |
} | |
// Public - show the tooltip on the screen | |
// | |
// Returns a tip | |
tip.show = function() { | |
var args = Array.prototype.slice.call(arguments) | |
if(args[args.length - 1] instanceof SVGElement) target = args.pop() | |
var content = html.apply(this, args), | |
poffset = offset.apply(this, args), | |
dir = direction.apply(this, args), | |
nodel = getNodeEl(), | |
i = directions.length, | |
coords, | |
scrollTop = document.documentElement.scrollTop || document.body.scrollTop, | |
scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft | |
nodel.html(content) | |
.style('position', 'absolute') | |
.style('opacity', 1) | |
.style('pointer-events', 'all') | |
while(i--) nodel.classed(directions[i], false) | |
coords = direction_callbacks[dir].apply(this) | |
nodel.classed(dir, true) | |
.style('top', (coords.top + poffset[0]) + scrollTop + 'px') | |
.style('left', (coords.left + poffset[1]) + scrollLeft + 'px') | |
return tip | |
} | |
// Public - hide the tooltip | |
// | |
// Returns a tip | |
tip.hide = function() { | |
var nodel = getNodeEl() | |
nodel | |
.style('opacity', 0) | |
.style('pointer-events', 'none') | |
return tip | |
} | |
// Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value. | |
// | |
// n - name of the attribute | |
// v - value of the attribute | |
// | |
// Returns tip or attribute value | |
tip.attr = function(n, v) { | |
if (arguments.length < 2 && typeof n === 'string') { | |
return getNodeEl().attr(n) | |
} else { | |
var args = Array.prototype.slice.call(arguments) | |
d3.selection.prototype.attr.apply(getNodeEl(), args) | |
} | |
return tip | |
} | |
// Public: Proxy style calls to the d3 tip container. Sets or gets a style value. | |
// | |
// n - name of the property | |
// v - value of the property | |
// | |
// Returns tip or style property value | |
tip.style = function(n, v) { | |
// debugger; | |
if (arguments.length < 2 && typeof n === 'string') { | |
return getNodeEl().style(n) | |
} else { | |
var args = Array.prototype.slice.call(arguments); | |
if (args.length === 1) { | |
var styles = args[0]; | |
Object.keys(styles).forEach(function(key) { | |
return d3.selection.prototype.style.apply(getNodeEl(), [key, styles[key]]); | |
}); | |
} | |
} | |
return tip | |
} | |
// Public: Set or get the direction of the tooltip | |
// | |
// v - One of n(north), s(south), e(east), or w(west), nw(northwest), | |
// sw(southwest), ne(northeast) or se(southeast) | |
// | |
// Returns tip or direction | |
tip.direction = function(v) { | |
if (!arguments.length) return direction | |
direction = v == null ? v : d3.functor(v) | |
return tip | |
} | |
// Public: Sets or gets the offset of the tip | |
// | |
// v - Array of [x, y] offset | |
// | |
// Returns offset or | |
tip.offset = function(v) { | |
if (!arguments.length) return offset | |
offset = v == null ? v : d3.functor(v) | |
return tip | |
} | |
// Public: sets or gets the html value of the tooltip | |
// | |
// v - String value of the tip | |
// | |
// Returns html value or tip | |
tip.html = function(v) { | |
if (!arguments.length) return html | |
html = v == null ? v : d3.functor(v) | |
return tip | |
} | |
// Public: destroys the tooltip and removes it from the DOM | |
// | |
// Returns a tip | |
tip.destroy = function() { | |
if(node) { | |
getNodeEl().remove(); | |
node = null; | |
} | |
return tip; | |
} | |
function d3_tip_direction() { return 'n' } | |
function d3_tip_offset() { return [0, 0] } | |
function d3_tip_html() { return ' ' } | |
var direction_callbacks = { | |
n: direction_n, | |
s: direction_s, | |
e: direction_e, | |
w: direction_w, | |
nw: direction_nw, | |
ne: direction_ne, | |
sw: direction_sw, | |
se: direction_se | |
}; | |
var directions = Object.keys(direction_callbacks); | |
function direction_n() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.n.y - node.offsetHeight, | |
left: bbox.n.x - node.offsetWidth / 2 | |
} | |
} | |
function direction_s() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.s.y, | |
left: bbox.s.x - node.offsetWidth / 2 | |
} | |
} | |
function direction_e() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.e.y - node.offsetHeight / 2, | |
left: bbox.e.x | |
} | |
} | |
function direction_w() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.w.y - node.offsetHeight / 2, | |
left: bbox.w.x - node.offsetWidth | |
} | |
} | |
function direction_nw() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.nw.y - node.offsetHeight, | |
left: bbox.nw.x - node.offsetWidth | |
} | |
} | |
function direction_ne() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.ne.y - node.offsetHeight, | |
left: bbox.ne.x | |
} | |
} | |
function direction_sw() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.sw.y, | |
left: bbox.sw.x - node.offsetWidth | |
} | |
} | |
function direction_se() { | |
var bbox = getScreenBBox() | |
return { | |
top: bbox.se.y, | |
left: bbox.e.x | |
} | |
} | |
function initNode() { | |
var node = d3.select(document.createElement('div')) | |
node | |
.style('position', 'absolute') | |
.style('top', 0) | |
.style('opacity', 0) | |
.style('pointer-events', 'none') | |
.style('box-sizing', 'border-box') | |
return node.node() | |
} | |
function getSVGNode(el) { | |
el = el.node() | |
if(el.tagName.toLowerCase() === 'svg') | |
return el | |
return el.ownerSVGElement | |
} | |
function getNodeEl() { | |
if(node === null) { | |
node = initNode(); | |
// re-add node to DOM | |
document.body.appendChild(node); | |
}; | |
return d3.select(node); | |
} | |
// Private - gets the screen coordinates of a shape | |
// | |
// Given a shape on the screen, will return an SVGPoint for the directions | |
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest), | |
// sw(southwest). | |
// | |
// +-+-+ | |
// | | | |
// + + | |
// | | | |
// +-+-+ | |
// | |
// Returns an Object {n, s, e, w, nw, sw, ne, se} | |
function getScreenBBox() { | |
var targetel = target || d3.event.target; | |
while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) { | |
targetel = targetel.parentNode; | |
} | |
var bbox = {}, | |
matrix = targetel.getScreenCTM(), | |
tbbox = targetel.getBBox(), | |
width = tbbox.width, | |
height = tbbox.height, | |
x = tbbox.x, | |
y = tbbox.y | |
point.x = x | |
point.y = y | |
bbox.nw = point.matrixTransform(matrix) | |
point.x += width | |
bbox.ne = point.matrixTransform(matrix) | |
point.y += height | |
bbox.se = point.matrixTransform(matrix) | |
point.x -= width | |
bbox.sw = point.matrixTransform(matrix) | |
point.y -= height / 2 | |
bbox.w = point.matrixTransform(matrix) | |
point.x += width | |
bbox.e = point.matrixTransform(matrix) | |
point.x -= width / 2 | |
point.y -= height / 2 | |
bbox.n = point.matrixTransform(matrix) | |
point.y += height | |
bbox.s = point.matrixTransform(matrix) | |
return bbox | |
} | |
return tip | |
}; |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset = "UTF-8"> | |
<link rel = "stylesheet" type = "text/css" href = "main.css"/> | |
<script src="//d3js.org/d3.v4.min.js"></script> | |
<script src="//d3js.org/d3-scale-chromatic.v1.min.js"></script> | |
<script src="d3-tip.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.js"></script> | |
<title> Test </title> | |
</head> | |
<body> | |
<header> | |
<div class="center"> | |
<select id="selectLinkNumber" name="selectLinkNumber"> | |
<option value="1">Select Number of Links</option> | |
<option value="2">Two or More Links</option> | |
<option value="3">Three or More Links</option> | |
<option value="4">Four Links</option> | |
</select> | |
<select id="s2" name="s2"> | |
<option value="1">Select Number of Links</option> | |
<option value="2">Links to Multiple Nodes</option> | |
</select> | |
</div> | |
</header> | |
<svg width="900" height="600"></svg> | |
<script src="script.js"></script> | |
</body> | |
</html> |
.links path { | |
fill: none; | |
} | |
.link.pro { | |
stroke: red; | |
} | |
.link.friend { | |
stroke: #20a064; | |
} | |
.link.enemy { | |
stroke: black; | |
} | |
nodes { | |
fill: red; | |
stroke: #333; | |
stroke-width: 1.5px; | |
} | |
text { | |
font-family: "Ariel"; | |
font-size: 16px; | |
pointer-events: none; | |
} | |
h1 { | |
color: black; | |
text-align:center; | |
font-style: underline; | |
font-size: 24px; | |
font-family: "Ariel"; | |
} | |
h3 { | |
color: black; | |
text-align:center; | |
font-style: italic; | |
font-size: 16px; | |
font-family: "Ariel"; | |
} | |
.d3-tip { | |
line-height: 1; | |
font-weight: normal; | |
padding: 12px; | |
background: rgba(0, 0, 0, 0.8); | |
color: #fff; | |
border-radius: 2px; | |
text-align: left; | |
} | |
/* Creates a small triangle extender for the tooltip */ | |
.d3-tip:after { | |
box-sizing: border-box; | |
display: inline; | |
font-size: 10px; | |
width: 100%; | |
line-height: 1; | |
color: rgba(0, 0, 0, 0.8); | |
content: "\25BC"; | |
position: absolute; | |
text-align: center; | |
} | |
/* Style northward tooltips differently */ | |
.d3-tip.n:after { | |
margin: -1px 0 0 0; | |
top: 100%; | |
left: 0; | |
} | |
span | |
{ | |
width:100px; | |
clear:right; | |
float:left; | |
text-align:left; | |
font-weight: bold; | |
padding-right:2px; | |
} | |
.center { | |
text-align: center; | |
} | |
body { | |
margin: 0; | |
padding: 0; | |
background: #eee; | |
} | |
.nav ul { | |
list-style: none; | |
background-color: #444; | |
text-align: center; | |
padding: 0; | |
margin: 0; | |
} | |
.nav li { | |
font-family: 'Oswald', sans-serif; | |
font-size: 1.2em; | |
line-height: 40px; | |
height: 40px; | |
border-bottom: 1px solid #888; | |
} | |
.nav a { | |
text-decoration: none; | |
color: #fff; | |
display: block; | |
transition: .3s background-color; | |
} | |
.nav a:hover { | |
background-color: #00C3FF; | |
} | |
.nav a.active { | |
background-color: #fff; | |
color: #444; | |
cursor: default; | |
} | |
@media screen and (min-width: 600px) { | |
.nav li { | |
width: 175px; | |
border-bottom: none; | |
height: 50px; | |
line-height: 50px; | |
font-size: 1.4em; | |
} | |
.nav li { | |
display: inline-block; | |
margin-right: -4px; | |
} | |
#content { | |
width:100%; | |
max-width:800px; | |
margin:0 auto; | |
text-align:center; | |
font-size: 20px; | |
} | |
.box { | |
display: inline-block; | |
height: 170px; | |
width: 170px; | |
margin:10px; | |
overflow:auto; | |
border-style: solid; | |
border-color: #444; | |
} |
var dropdown = d3.select("#selectLinkNumber") | |
var s2 = d3.select("#s2") | |
var val = dropdown.node().options[dropdown.node().selectedIndex].value; | |
var val2 = s2.node().options[s2.node().selectedIndex].value; | |
var change = function() { | |
d3.selectAll("svg > *").remove() | |
d3.json("test.json", function(error, graph) { | |
if (error) throw error; | |
var filteredLinks = graph.links.filter(d => d.linkCount >= val || d.risk >= val2); | |
var filteredNodes = Object.values(filteredLinks.reduce(function(t,v){ | |
if(!t[v.source]){ | |
t[v.source] = graph.nodes.filter(o => o.id === v.source)[0] | |
} | |
if(!t[v.target]){ | |
t[v.target] = graph.nodes.filter(o => o.id === v.target)[0] | |
} | |
return t; | |
},{})) | |
var filteredGraph = { | |
nodes: filteredNodes, | |
links: filteredLinks | |
}; | |
var svg = d3.select("svg"), | |
width = +svg.attr("width"), | |
height = +svg.attr("height"); | |
var zoomHandler = d3.zoom() | |
.on("zoom", zoomActions); | |
zoomHandler(svg); | |
var g = svg.append("g").call(zoomHandler); //Creates group for zoom | |
var simulation = d3.forceSimulation() | |
.force("forceX", d3.forceX().strength(.4).x(width * .5)) | |
.force("forceY", d3.forceY().strength(.4).y(height * .5)) | |
.force("link", d3.forceLink().id(function(d) { return d.id; })) | |
.force("charge", d3.forceManyBody().strength(-25)) | |
.force("center", d3.forceCenter(width / 2, height / 2)); | |
var tip = d3.tip() | |
.attr('class', 'd3-tip') | |
.offset([-10, 0]) | |
.html(function(d) { | |
return "<div class=center>" + "<img src=" + d.image + " height=100 width=100>" + "</div>" | |
+ "<br>" + "<span>" + "ID:" + "</span>" + d.id | |
+ "<br>" + "<span>" + "Type:" + "</span>" + d.type | |
}); | |
g.call(tip); | |
var link = g.append("g") | |
.selectAll("path") | |
.data(filteredGraph.links) | |
.enter().append("path") | |
.attr("class", function(d) { return "link " + d.type; }) | |
.attr("stroke-width", function(d) { return d.linkCount; }) | |
.style("fill", "none"); | |
var nodeColor = d3.scaleOrdinal() | |
.domain(["boss", "employee"]) | |
.range(["#005495", "#00C3FF"]); | |
var node = g.append("g") | |
.selectAll("circle") | |
.data(filteredGraph.nodes) | |
.enter().append("circle") | |
.style("fill", function(d) { return nodeColor(d.type); }) | |
.attr("r", 2.5) | |
.call(d3.drag() | |
.on("start", dragstart) | |
.on("drag", dragged) | |
.on("end", dragend)) | |
.on('mouseover', tip.show) | |
.on('mouseout', tip.hide) | |
.on('click', connectedNodes) | |
.on("dblclick", releasenode); | |
simulation | |
.nodes(filteredGraph.nodes) | |
.on("tick", tick); | |
simulation.force("link") | |
.links(filteredGraph.links); | |
var nodeLeg = d3.scaleOrdinal() | |
.domain(["NodeType1", "NodeType2", "NodeType3", "NodeType4"]) | |
.range(["#005495", "#00C3FF", "#FF0000", "#FFC0CB"]); | |
svg.append("g") | |
.attr("class", "nodeLegendOrdinal") | |
.attr("transform", "translate(20,15)"); | |
var nodeLegendOrdinal = d3.legendColor() | |
.shape("path", d3.symbol().type(d3.symbolCircle).size(125)()) | |
.title("Employee Type") | |
.scale(nodeLeg); | |
svg.select(".nodeLegendOrdinal") | |
.call(nodeLegendOrdinal); | |
var linkLeg = d3.scaleOrdinal() | |
.domain(["LinkType1", "LinkType2", "LinkType3", "LinkType4"]) | |
.range(["black", "#20a064", "#F45D01", "#005495"]); | |
svg.append("g") | |
.attr("class", "linkLegendOrdinal") | |
.attr("transform", "translate(20,125)"); | |
var linkLegendOrdinal = d3.legendColor() | |
.shape("rect") | |
.shapeWidth(25) | |
.shapeHeight(5) | |
.title("Link Type") | |
.scale(linkLeg); | |
svg.select(".linkLegendOrdinal") | |
.call(linkLegendOrdinal); | |
var toggle = 0; | |
var linkedByIndex = {}; | |
for (i = 0; i < filteredGraph.nodes.length; i++) { | |
linkedByIndex[i + "," + i] = 1; | |
}; | |
filteredGraph.links.forEach(function (d) { | |
linkedByIndex[d.source.index + "," + d.target.index] = 1; | |
}); | |
function neighboring(a, b) { | |
return linkedByIndex[a.index + "," + b.index]; | |
} | |
function connectedNodes() { | |
if (toggle == 0) { | |
d = d3.select(this).node().__data__; | |
node.style("opacity", function (o) { | |
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1; | |
}); | |
link.style("opacity", function (o) { | |
return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1; | |
}); | |
toggle = 1; | |
} else { | |
node.style("opacity", 1); | |
link.style("opacity", 1); | |
toggle = 0; | |
} | |
} | |
filteredGraph.links.sort(function(a,b) { | |
if (a.source > b.source) {return 1;} | |
else if (a.source < b.source) {return -1;} | |
else { | |
if (a.target > b.target) {return 1;} | |
if (a.target < b.target) {return -1;} | |
else {return 0;} | |
} | |
}); | |
for (var i=0; i<filteredGraph.links.length; i++) { | |
if (i != 0 && | |
filteredGraph.links[i].source == filteredGraph.links[i-1].source && | |
filteredGraph.links[i].target == filteredGraph.links[i-1].target) { | |
filteredGraph.links[i].linknum = filteredGraph.links[i-1].linknum + 1; | |
} | |
else {filteredGraph.links[i].linknum = 1;}; | |
}; | |
function tick() { | |
link.attr("d", linkArc); | |
node.attr("transform", transform); | |
} | |
function linkArc(d) { | |
var curve=2; | |
var homogeneous=3.2; | |
var dx = d.target.x - d.source.x, | |
dy = d.target.y - d.source.y, | |
dr = Math.sqrt(dx*dx+dy*dy)*(d.linknum+homogeneous)/(curve*homogeneous); //linknum is defined above | |
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; | |
} | |
function transform(d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
} | |
function dragstart(d, i) { | |
simulation.stop() | |
} | |
function dragged(d) { | |
d.fx = d3.event.x; | |
d.fy = d3.event.y; | |
} | |
function dragend(d, i) { | |
simulation.alpha(0.3).restart(); | |
} | |
function releasenode(d) { | |
d.fx = null; | |
d.fy = null; | |
} | |
function zoomActions(){ | |
g.attr("transform", d3.event.transform) | |
} | |
}) | |
} | |
dropdown.on("change", change) | |
s2.on("change", change) | |
change(); |
{"nodes": [{"id": "Michael Scott", "type": "boss"} | |
,{"id": "Jim Halpert", "type": "employee"} | |
,{"id": "Pam Beasley", "type": "employee"} | |
,{"id": "Kevin Malone", "type": "employee"} | |
,{"id": "Angela", "type": "employee"} | |
,{"id": "Dwight Schrute", "type": "employee"}] | |
,"links": [{"source": "Michael Scott", "target": "Jim Halpert", "linkCount": 1, "val2": 2, "type": "pro"} | |
,{"source": "Pam Beasley", "target": "Kevin Malone", "linkCount": 2, "val2": 2, "type": "friend"} | |
,{"source": "Pam Beasley", "target": "Kevin Malone", "linkCount": 2, "val2": 2, "type": "pro"} | |
,{"source": "Angela", "target": "Dwight Schrute", "linkCount": 3, "val2": 1, "type": "friend"} | |
,{"source": "Angela", "target": "Dwight Schrute", "linkCount": 3, "val2": 1, "type": "enemy"} | |
,{"source": "Angela", "target": "Dwight Schrute", "linkCount": 3, "val2": 1, "type": "pro"}] | |
} |