Skip to content

Instantly share code, notes, and snippets.

@mwdchang
Last active August 29, 2015 14:17
Show Gist options
  • Save mwdchang/67f3c0254585291f7b4b to your computer and use it in GitHub Desktop.
Save mwdchang/67f3c0254585291f7b4b to your computer and use it in GitHub Desktop.
Demo for Aly
<!DOCTYPE HTML>
<html>
<head>
<title> Drag/Link Test </title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.2/underscore-min.js"></script>
<style>
body {
margin: 1em;
font-family: Tahoma;
font-size: 0.8em;
}
path.link {
fill: none;
stroke: #333;
stroke-width: 6px;
cursor: default;
}
text.label {
font-family: Tahoma;
font-weight: bold;
fill: #E8E8E8;
}
</style>
</head>
<body>
<div style="display:inline-block" id="canvas">
</div>
<div style="display:inline-block; vertical-align:top">
Right Panel
<hr>
<div id="details"></div>
</div>
</body>
<script>
var colour_normal = '#7799FF';
var colour_hover = '#EE6611';
var colour_selected = '#BB8822';
var nodeCounter = 0;
var nodes = [];
var edges = [];
var h = 600, w = 500;
var svg = d3.select('#canvas').append('svg').attr('width', w).attr('height', h);
var defs = svg.append('svg:defs');
var linkGroup;
var nodeHover = null;
var nodeSelected = null;
var selectedNodeId = -1;
svg.append('rect')
.attr('width', w)
.attr('height', h)
.style('fill', '#EEEEEE');
linkGroup = svg.append('g');
// define arrow markers for leading arrow
defs.append('svg:marker')
.attr('id', 'mark-end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 7)
.attr('markerWidth', 3.5)
.attr('markerHeight', 3.5)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5');
defs.append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 18)
.attr('markerWidth', 3.5)
.attr('markerHeight', 3.5)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5');
// Only for dragging
var draggingEdge = svg.append('svg:path')
.attr('class', 'link dragline ')
.attr('d', 'M0,0L0,0')
.style('marker-end', 'url(#mark-end-arrow)');
function _line(x1, y1, x2, y2) {
return 'M' + x1 + ',' + y1 + 'L' + x2 + ',' + y2;
}
// TODO: Not efficient, especially for labels
function updateAllEdges() {
d3.selectAll('.edge').remove();
// Rebuild
nodes.forEach(function(node) {
console.log(node);
node.children.forEach(function(child) {
linkGroup.append('svg:path')
.attr('class', 'link dragline edge')
.attr('d', _line(node.x, node.y, child.x, child.y))
.style('marker-end', 'url(#end-arrow)');
});
});
// Rebulid labels
d3.selectAll('.label').remove();
d3.selectAll('.node').each(function(d) {
svg.append('text').attr('class', 'label').attr('x', d.x - 4).attr('y', d.y + 4).text(function() {
return d.id;
});
});
}
function propagateNode(node) {
}
function removeNode(node) {
}
function removeEdge(from, to) {
}
function _dist(x1, y1, x2, y2) {
return Math.sqrt( (x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
}
// This can have duplicates
function _getAncestors(node) {
console.log('checking', node.id);
if (!node.parents || node.parents.length === 0) {
return [];
}
var ancestors = [];
node.parents.forEach(function(p) {
console.log('adding ancestor ', p.id);
ancestors.push(p);
ancestors = ancestors.concat( _getAncestors(p) );
});
console.log(node.id, ancestors);
return ancestors;
}
function getAncestors(node) {
var ancestors = [];
}
// When dragging is performed on a node
var circleDrag = d3.behavior.drag()
.origin(function(d) {
console.log('in drag', d, d.x, d.y);
return {x: d.x, y: d.y};
})
.on('drag', function(d) {
var coord = d3.mouse(this);
var shiftKey = d3.event.sourceEvent.shiftKey;
if (shiftKey) {
draggingEdge.style('visibility', 'visible');
draggingEdge.attr('d', 'M' + d.x + ',' + d.y + 'L' + coord[0] + ',' + coord[1]);
} else {
d3.select(this).attr('cx', coord[0]).attr('cy', coord[1]);
d3.select(this).datum().x = coord[0];
d3.select(this).datum().y = coord[1];
updateAllEdges();
}
})
.on('dragend', function(d) {
draggingEdge.style('visibility', 'hidden');
// TODO: Need to check for cycles
if (nodeHover) {
var from = d;
var to = nodeHover;
if (_.pluck(from.children, 'id').indexOf(to.id) >= 0 || from.id === to.id) {
console.warn('Relationship already exist between ' + from.id + ' and ' + to.id);
return;
} else {
console.log('Relation should go from ', d, ' to ', nodeHover);
from.children.push( to );
to.parents.push( from );
linkGroup.append('svg:path')
.attr('class', 'link dragline edge')
.attr('d', _line(from.x, from.y, to.x, to.y))
.style('marker-end', 'url(#end-arrow)');
}
}
});
svg.on('mousemove', function() {
var coord = d3.mouse(this);
});
svg.on('mousedown', function() {
if (selectedNodeId != -1) {
selectedNodeId = -1;
d3.selectAll('circle').style('fill', '#7799FF');
d3.select('#details').selectAll('*').remove();
return;
}
var coord = d3.mouse(this);
d3.event.stopPropagation();
// Don't allow node to be created too close to each other to prevent
// weird interactive behaviours if elements are overlapped.
var nodeTooClose = false;
nodes.forEach(function(node) {
if ( _dist(node.x, node.y, coord[0], coord[1]) < 40) nodeTooClose = true;
});
if (nodeTooClose === true) return;
nodeCounter ++;
var newNode = {
id:nodeCounter,
x:coord[0],
y:coord[1],
children:[],
parents:[],
filter: {id:nodeCounter}
};
nodes.push(newNode);
svg.append('circle')
.datum(newNode)
.attr('class', 'node')
.attr('cx', coord[0])
.attr('cy', coord[1])
.attr('r', 20)
.style('stroke', '#888')
.style('stroke-width', 2)
.style('fill', '#7799FF')
.on('mouseover', function(d) {
nodeHover = d;
d3.select(this).style('fill', colour_hover);
})
.on('mouseout', function(d) {
if (selectedNodeId === d.id) {
d3.select(this).style('fill', colour_selected);
} else {
d3.select(this).style('fill', colour_normal);
}
})
.on('mousedown', function(d) {
d3.event.stopPropagation();
if (d3.event.shiftKey) return;
// Reset any previously selected node. Should probably use class
if (selectedNodeId > 0) {
d3.selectAll('circle').filter(function(d) { return d.id === selectedNodeId; }).style('fill', '#7799FF');
}
d3.select(this).style('fill', colour_selected);
selectedNodeId = d.id;
var details = d3.select('#details');
details.selectAll('*').remove();
details.append('div').text('ID:' + d.id);
details.append('div').text('X-Pos:' + Math.floor(d.x));
details.append('div').text('Y-Pos:' + Math.floor(d.y));
details.append('div').text('Children: [' + _.pluck(d.children, 'id') + ']');
details.append('div').text('Parents: [' + _.pluck(d.parents, 'id') + ']');
details.append('div').text('Ancestors: [' + _.pluck(_getAncestors(d), 'id') + ']');
details.append('span').text('Filter:');
details.append('input').attr('size', 80).attr('value', JSON.stringify(d.filter));
details.append('div');
details.append('button').text('Update').on('click', function() {
console.log('updating filter for ' + d.id + ' to ' + d3.select('input').node().value );
// TODO: check
d.filter = JSON.parse( d3.select('input').node().value);
});
})
.call(circleDrag);
updateAllEdges();
});
d3.select(window).on('keydown', function() {
console.log(d3.event.keyCode);
if (d3.event.keyCode === 46 || d3.event.keyCode === 8) { // delete or backspace
d3.event.preventDefault();
if (selectedNodeId > 0) {
console.log('delete key pressed');
// Delete node
// 1) remove references
nodes.forEach(function(node) {
var index = -1;
index = _.pluck(node.children, 'id').indexOf( selectedNodeId );
if (index >= 0) {
node.children.splice(index, 1);
}
index = _.pluck(node.parents, 'id').indexOf( selectedNodeId );
if (index >= 0) {
node.parents.splice(index, 1);
}
});
// 2) update graphics
d3.selectAll('circle').filter(function(d) { return d.id === selectedNodeId; }).remove();
// 3) delete node
var index = _.pluck(nodes, 'id').indexOf(selectedNodeId);
nodes.splice(index, 1);
// 4) update edges
updateAllEdges();
selectedNodeId = -1;
}
}
});
</script>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment