Skip to content

Instantly share code, notes, and snippets.

@biovisualize
Created December 5, 2012 07:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save biovisualize/4213367 to your computer and use it in GitHub Desktop.
Save biovisualize/4213367 to your computer and use it in GitHub Desktop.
Reusable Circular Network with Editable Data Table
var Table = function module() {
var dispatch = d3.dispatch("edit");
function exports(_selection) {
_selection.each(function (_dataset) {
//________________________________________________
// Data
//________________________________________________
var data = _dataset[0];
var columnNames = _dataset[1];
//________________________________________________
// Table
//________________________________________________
var table = d3.select(this).selectAll("table").data([0]);
table.enter().append('table')
.append('tr')
.attr('class', 'header-row')
.selectAll('th')
.data(columnNames)
.enter().append('th')
.attr('class', 'header .no-select')
.text(function(d, i){return d;});
var rows = table.selectAll('tr.row')
.data(data);
rows.enter().append('tr')
.attr('class', 'row');
var cells = rows.selectAll('td.cell')
.data(function(d, i){return d;})
cells.enter().append('td')
.attr({class: 'cell', contenteditable: true});
cells.text(function(d, i){return d;})
.on("keyup", function(d, i){
var newData = [];
d3.select('.table').selectAll('tr.row').selectAll('td')
.each(function(d, i, pI){
var text = d3.select(this).text();
if (typeof newData[pI] == 'undefined') newData[pI] = [];
newData[pI].push(text)
});
dispatch.edit(newData);
});
});
}
d3.rebind(exports, dispatch, "on");
return exports;
};
var CircularNetwork = function module() {
var opts = {
width: 200,
height: 200,
margins: {top:30, right:30, bottom:30, left:30},
fillColorList: [
"#3182bd", "#6baed6", "#9ecae1", "#c6dbef",
"#e6550d", "#fd8d3c", "#fdae6b", "#fdd0a2",
"#31a354", "#74c476", "#a1d99b", "#c7e9c0",
"#756bb1", "#9e9ac8", "#bcbddc", "#dadaeb",
"#636363", "#969696", "#bdbdbd", "#d9d9d9"
],
enableTooltips: true,
enableBringToFront: true,
labelOffset: 5
};
function exports(_selection) {
_selection.each(function (_dataset) {
//________________________________________________
// Data
//________________________________________________
var dataset = d3.transpose(_dataset);
var start = dataset[0];
var end = dataset[1];
var weight = (dataset[2] == undefined)
? start.map(function(d, i){return 1;})
: dataset[2].map(function(d){return parseInt(d);});
//________________________________________________
// Data transform
//________________________________________________
var transformedData = d3.transpose([start, end, weight]);
// list of unique values to use as index
var unique = [];
transformedData.forEach(function(d, i){
if(unique.indexOf(d[0]) == -1) unique.push(d[0]);
if(unique.indexOf(d[1]) == -1) unique.push(d[1]);
});
// init square matrix
var matrix = d3.range(unique.length).map(function(){
return d3.range(unique.length).map(function(){return 0;});
});
// compute matrix
transformedData.forEach(function(d, i){
var row = unique.indexOf(d[1]);
var col = unique.indexOf(d[0]);
matrix[col][row] = d[2];
});
//________________________________________________
// DOM selection
//________________________________________________
var chartW = Math.max(opts.width - opts.margins.left - opts.margins.right, 0.1);
var chartH = Math.max(opts.height - opts.margins.top - opts.margins.bottom, 0.1);
var svg = d3.select(this).selectAll("svg").data([0]);
svg.enter().append("svg").attr({width: opts.width, height: opts.height})
.append("g").attr({class: "vis-group",
transform: "translate(" + (opts.margins.left + chartW/2) + "," + (opts.margins.top + chartH/2) + ")"});
var chartSVG = d3.select("g.vis-group");
//________________________________________________
// Circular network
//________________________________________________
var fill = function(i) {
return opts.fillColorList[i % opts.fillColorList.length];
}
var chord = d3.layout.chord().padding(.05).matrix(matrix);
var r1 = Math.min(chartW, chartH) / 2;
var r0 = r1 * 0.9;
// draw arcs
var groupSVG = chartSVG.selectAll("path.arc")
.data(chord.groups);
groupSVG.enter().append("path")
.attr("class", "arc")
groupSVG.attr("d", d3.svg.arc()
.innerRadius(r0)
.outerRadius(r1))
.style("fill", function(d){ return fill(d.index);});
groupSVG.exit().remove();
// draw chords
var chordSVG = chartSVG.selectAll("path.link")
.data(chord.chords);
chordSVG.enter()
.append("path")
.attr("class", "link")
.call(highlight);
chordSVG.attr("d", d3.svg.chord().radius(r0))
.style("fill",function (d) {return fill(d.target.index);});
chordSVG.exit().remove();
if(opts.enableBringToFront) chordSVG.call(bringToFront);
function highlight(_selection){
_selection
.on("mouseover.highlight", function(d, i) {
d3.select(this).classed({highlighted: true});
})
.on("mouseout.highlight", function(d, i){
d3.select(this).classed({highlighted: false});
});
}
function bringToFront(_selection){
_selection.on("mouseover.toFront", function() {
var dragTarget = event.target;
dragTarget.parentNode.appendChild( dragTarget );
});
}
//________________________________________________
// Labels
//________________________________________________
var labelSVG = chartSVG.selectAll("text.label")
.data(chord.groups);
labelSVG.enter().append("text")
.attr("class", "label");
labelSVG.each(function(d){ d.angle = (d.startAngle + d.endAngle) / 2; })
.attr("text-anchor", function(d){ return d.angle > Math.PI ? "end" : null; })
.attr("transform", function(d){
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
+ "translate(" + (r1 + opts.labelOffset) + ")"
+ (d.angle > Math.PI ? "rotate(180)" : "");
})
.text(function(d, i){ return unique[i];});
labelSVG.exit().remove();
//________________________________________________
// Tooltips
//________________________________________________
if(opts.enableTooltips){
var groupTooltip = tooltip()
.accessor(function(d, i){
return "<strong>start</strong>: " + unique[i]; });
var chordTooltip = tooltip()
.accessor(function(d, i){
var tipText = "<strong>start</strong>: " + unique[d.source.index] + "<br/> "
+ "<strong>weight</strong>: " + d.source.value + "<br/> "
+ "to<br/>"
+ "<strong>end</strong>: " + unique[d.target.index] + "<br/>"
+ "<strong>weight</strong>: " + d.target.value;
return tipText;
});
groupSVG.call(groupTooltip);
chordSVG.call(chordTooltip);
}
});
}
exports.opts = opts;
createAccessors(exports);
return exports;
};
var tooltip = function module(){
var opts = {
accessor: function(d, i){return d;}
};
function exports(selection){
var tooltipDiv;
var body = d3.select("body");
selection.on("mouseover.tooltip", function(d, i){
d3.select("body").selectAll("div.tooltip").remove();
tooltipDiv = body.append("div").attr("class", "tooltip");
var absoluteMousePos = d3.mouse(body.node());
tooltipDiv.style("left", (absoluteMousePos[0] + 10)+"px")
.style("top", (absoluteMousePos[1] - 15)+"px")
.style("position", "absolute")
.style("z-index", 1001);
var tooltipText = opts.accessor(d, i) || "";
tooltipDiv.html(tooltipText);
})
.on("mousemove.tooltip", function(d, i) {
var absoluteMousePos = d3.mouse(body.node());
tooltipDiv.style("left", (absoluteMousePos[0] + 10)+"px")
.style("top", (absoluteMousePos[1] - 15)+"px");
})
.on("mouseout.tooltip", function(d, i){
tooltipDiv.remove();
});
};
exports.opts = opts;
createAccessors(exports);
return exports;
};
function createAccessors(visExport) {
for (var n in visExport.opts) {
if (!visExport.opts.hasOwnProperty(n)) continue;
visExport[n] = (function(n) {
return function(v) {
return arguments.length ? (visExport.opts[n] = v, this) : visExport.opts[n];
}
})(n);
}
};
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js"></script>
<script type="text/javascript" src="./circular_network.js"></script>
<script type="text/javascript" src="./basic-editable-table.js"></script>
<style>
.reset-button{
width: 50px;
height: 25px;
border: 1px solid black;
background-color: white;
cursor: pointer;
text-align: center;
position: absolute;
top: 260px;
}
.reset-button:hover{
background-color: #eee;
}
.reset-button:active{
background-color: #ccc;
}
.no-select{
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
table{
border-collapse: collapse;
}
.table{
position: absolute;
top: 100px;
}
.container{
position: absolute;
top: 0;
left: 250px;
}
td, th{
border: 1px solid black;
background-color: white;
width: 70px;
padding: 5px;
text-align: left;
}
th.header{
cursor: default;
}
.tooltip{
background-color: aliceblue;
pointer-events: none;
margin-left: 10px;
padding: 10px;
opacity: 0.8;
}
svg text.label{
fill: black;
font: 10px Verdana;
}
svg path.arc{
stroke: grey;
}
svg path.link{
stroke: grey;
opacity: 0.9;
}
.highlighted{
opacity: 1 !important;
}
</style>
</head>
<body>
<div class="table"></div>
<div class="reset-button no-select">Reset</div>
<div class="container"></div>
<script type="text/javascript">
// Data
var data = [
['page0', 'page1', 200],
['page1', 'page0', 50],
['page2', 'page0', 100],
['page0', 'page2', 100]
];
var columnNames = ['Start', 'End', 'Weight']
// Charts
var table = Table()
.on('edit', function(d){ update(d) });
var circularNetwork = CircularNetwork()
.width(500)
.height(500)
.margins({top:50, right:50, bottom:50, left:50})
.enableTooltips(true)
.labelOffset(5);
// Bind charts to DOM
function update(_data){
d3.select('.container')
.datum(_data)
.call(circularNetwork);
}
function updateTable(_data){
d3.select('.table')
.datum(_data)
.call(table);
}
update(data);
updateTable([data, columnNames])
// Update charts on reset
d3.select('.reset-button')
.on("mouseup", function(d, i){
update(data);
updateTable([data, columnNames])
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment