Skip to content

Instantly share code, notes, and snippets.

Created July 6, 2017 20:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anonymous/1d6ce91145cde3e9e6c409f627463d9a to your computer and use it in GitHub Desktop.
Save anonymous/1d6ce91145cde3e9e6c409f627463d9a to your computer and use it in GitHub Desktop.
Multiple Dropdown Filters on one Force-Directed Graph
license: mit
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">All 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">All</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 change = function() {
d3.selectAll("svg > *").remove()
var val = dropdown.node().options[dropdown.node().selectedIndex].value;
var otherVal = s2.node().options[s2.node().selectedIndex].value;
d3.json("test.json", function(error, graph) {
if (error) throw error;
var filteredLinks = graph.links.filter(d => d.linkCount >= val);
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"}]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment