/*
* IF YOU WANT TO MODIFY THIS FILE DON'T USE CLOSURES
* Protovis examples use closure expressions regularly ex: .top(function(d) d * 2)
* with no {} curly braces. Scripts with this syntax will only work in Firefox since there is no way to modify
* the type attribute of JavaScipt tags in the header of Drupal pages.
* USE A REAL FUNCTION. EX: .top(function(d){ return d * 2}) INSTEAD.
* Protovis uses a special type attribute (instead of type="text/javascript" => type="text/javascript+protovis"
* that allows other browser to deal with closure expressions. See 2) on this page:
* http://mbostock.github.com/protovis/docs/start.html
*/
function display_traceability_diagram(data){
// Nest data with the same trace_id
for (var i in data) {
var trace_parents = [];
for (var j in data) {
if (data[i].trace_id == data[j].trace_id) {
trace_parents.push(data[j].parent_traces);
if (i != j) {
data.splice(j, 1);
}
}
}
data[i].parent_traces = trace_parents;
}
var assignment_submissions = distill_assignment_submissions(data, 'submission_parent_id');
// Reformat data into key/value pairs
var new_original_traces = {}
for (var i in data) {
new_original_traces[data[i].trace_id] = data[i];
}
// Get a list of assignment submissions
function distill_assignment_submissions(data, group) {
var assignment_submissions = []
var assignment_ids = {}
for (var i in data) {
if (!assignment_ids[data[i][group]]) {
assignment_submissions.push({
id: data[i][group],
title: data[i].submission_parent,
due: data[i].due,
submitted: data[i].date_time
});
assignment_ids[data[i][group]] = true;
}
}
return assignment_submissions;
}
// Nest Data
new_traces = convertToHierarchy(new_original_traces);
new_traces = paddHierarchy(new_traces, assignment_submissions, 'submission_parent_id', new_original_traces);
// Generate Tags
var new_tags = []
for (var i in new_original_traces) {
for (j in new_original_traces[i]['terms']) {
var tag_exists = false;
for (k = 0; k < new_tags.length; k++) {
if (new_tags[k].tag == new_original_traces[i]['terms'][j]) {
// increment the frequency
new_tags[k].frequency++;
tag_exists = true;
}
}
if (!tag_exists) {
new_tags.push({tag: new_original_traces[i]['terms'][j], frequency: 1});
}
}
}
// Global Variables
var frequencyRange = getTagFrequencyRange(new_tags);
var minTagFontSize = 12;
var maxTagFontSize = 30;
// Diagram Set Up Variables
var panelWidth = 500;
var panelHeight = 500;
var panelLeft = 0;
var panelRight = 0;
var panelTop = 0;
var panelBottom = 0;
var treeBreadth = 40; //This can be 360/number of nodes in that level
var nodeSizeSmall = 20;
var nodeSizeLarge = 60;
var ringSpacing = 32;
var numberRings = assignment_submissions.length;
var nodeLineWidthWide = 3;
var nodeLineWidthThin = 1;
// Diagram Colors Hex Values with #
var colorDefault = '#000000';
var colorHighlight = '#FFDF00';
var colorTermination = '#B2DF8A';
var colorFulfillment = '#33A023';
var colorForgotten = '#FF1d25';
var colorContinuation = '#FFFFFF';
var colorTagHighlight = '#1F78B4';
drawTagCloud(new_tags);
// Instantiate a protovis Panel
var vis = new pv.Panel()
.canvas($('#onion-diagram').get(0))
.width(panelWidth)
.height(panelHeight)
.left(panelLeft)
.right(panelRight)
.top(panelTop)
.bottom(panelBottom)
.events("all")
.event("mousemove", pv.Behavior.point());
// Add a panel to the vis for rings
var ringsPanel = vis.add(pv.Panel);
// Add radial tree to panel
var tree = vis.add(pv.Layout.Tree)
.nodes(pv.dom(new_traces).root("traces").nodes())
.depth(function(){ return ringSpacing; })
.breadth(treeBreadth)
.orient("radial");
// Add lines between node in radial tree
var treeLines = tree.link.add(pv.Line)
.strokeStyle(function(node, link) { return returnLineColor(node, link); })
.lineWidth(function(node, link) { return returnLineWidth(node, link); });
// Add dots to nodes in tree diagram
var treeNodes = tree.node.add(pv.Dot)
// variables for each node
.def('tag', 'nan')
.def('selected',-1)
.def('highlightedNodeIndex', -1)
.def('lastSelectedNode', -1)
.def('highlightdepth', -1)
// Node style (changes on mouseover/out/click events or explicit render())
.fillStyle(function(node) { return returnNodeFillStyle(node); })
.strokeStyle(function(node) { return returnNodeStrokeStyle(node); })
.size(function(node) { return returnNodeSize(node); })
.lineWidth(function(node) { return returnNodeLineWidth(node); })
// Events
.event('mouseover', function(node) {
// Set variables for rendering changes to the vis
node.nodeValue.highlight = 1;
this.highlightedNodeIndex(node.index);
this.highlightdepth(node.nodeValue.depth);
rings.highlightedIndex(node.nodeValue.depth);
// Re-render the lines and labels
treeLines.render();
treeLabels.render();
rings.render();
return this.highlightedNodeIndex(node.index);;
})
.event('mouseout', function(node) {
// reset the variables for rendering changes to the vis
node.nodeValue.highlight = -1;
this.highlightedNodeIndex(-1);
this.highlightdepth(-1);
rings.highlightedIndex(-1);
// Re-render the lines and labels
treeLines.render();
treeLabels.render();
rings.render();
return this.highlightdepth(-1);
})
.event('click', function(node) {
// Change the last selected node's "selected" attribute to 0
if (this.lastSelectedNode() != -1) {
this.lastSelectedNode().nodeValue.selected = 0;
}
// Change this node's "selected" attribute to 1
node.nodeValue.selected = 1;
// Save this node to the lastSelectedNode variable in
this.lastSelectedNode(node);
rings.lastSelectedIndex(node.nodeValue.depth);
// Re-render the lines
treeLines.render();
rings.render();
// Modify the detailed information in the div#trace-data
updateTraceData(node.nodeName);
return this.selected(node.index);
});
//Assign the each trace object to it's corresponding nodeValue
treeNodes.nodeValue= (function(node) { return new_original_traces[node.nodeName]; });
// Add labels to tree
var treeLabels = tree.label.add(pv.Label)
.visible(function(n){
if (n.index == treeNodes.highlightedNodeIndex()) {
return true;
} else {
return false;
}
})
.text(function(n) { return new_original_traces[n.nodeName] ? new_original_traces[n.nodeName].title : ""; })
.textBaseline("middle")
.textAngle(0);
vis.render();
// Add rings to rings panel in tree diagram
var rings = ringsPanel.add(pv.Wedge)
// variables
.def('highlightedIndex',-1)
.def('lastSelectedIndex',-1)
// data
.data(function() { return returnRingDataArray(); })
// ring layout
.left((panelWidth/2))
.bottom((panelHeight/2))
.startAngle(Math.PI)
.angle(2 * Math.PI)
.innerRadius(function() { return returnRingInnerRadius(); })
.outerRadius(function() { return returnRingOuterRadius(); })
.fillStyle(function() { return returnRingFillStyle(); })
// Events
.event('mouseover', function() {
this.highlightedIndex(this.index);
this.render();
}) // override
.event('mouseout', function() {
this.highlightedIndex(-1);
this.render();
}); // restore
// Make nodes that don't exist in the original JSON invisible
treeNodes.visible(function(n) { return (n.parentNode && new_original_traces[n.nodeName]) ? true : false; });
// Make lines that don't connect nodes that exist in the original JSON invisible
treeLines.visible(function(node, link){
if (node.parentNode) {
var parentNode = node.parentNode;
while (parentNode) {
if (new_original_traces[parentNode.nodeName]) {
return true;
}
if (parentNode.parentNode){
parentNode = parentNode.parentNode;
} else {
parentNode = false;
}
}
return false;
} else {
return false;
}
});
vis.render();
// Diagram Layout Functions
function returnRingFillStyle() {
if(rings.highlightedIndex() == rings.index){
return colorHighlight;
} else {
return colorDefault;
}
}
function returnRingDataArray() {
ringData = [];
for (var i = 0; i <= numberRings; i++) {
if (i == 0) {
ringData.push(0);
} else {
ringData.push(1);
}
}
return ringData;
}
function returnRingInnerRadius() {
var radius = rings.index * ringSpacing;
return radius;
}
function returnRingOuterRadius() {
var offset = 1;
var ringIndex = parseInt(rings.index);
var selectedDepth = rings.lastSelectedIndex();
var highlightedDepth = rings.highlightedIndex();
if(
// a node on this ring is selected
(selectedDepth == ringIndex) ||
// ... or a node on this ring is highlighted
(highlightedDepth == ringIndex) ||
// ..or this ring is being hovered over
(rings.highlightedIndex() == ringIndex)
) {
offset = 3;
}
return ringIndex * ringSpacing + offset;
}
function returnNodeLineWidth(node) {
if (new_original_traces[node.nodeName] && ($.inArray(treeNodes.tag(), new_original_traces[node.nodeName].terms) != -1) ) {
return nodeLineWidthWide;
} else {
return nodeLineWidthThin;
}
}
function returnNodeSize(node) {
//End nodes already had a nodeValue
if (!node.nodeValue || node.nodeValue=="end") {
node.nodeValue = {selection:"0",highlight:"0", depth:0};
if (node.parentNode) {
node.nodeValue.depth = node.parentNode.nodeValue.depth + 1;
}
}
if (treeNodes.selected()==treeNodes.index) {
return nodeSizeLarge;
} else {
return nodeSizeSmall;
}
}
function returnNodeStrokeStyle(node) {
if (new_original_traces[node.nodeName] && ($.inArray(treeNodes.tag(), new_original_traces[node.nodeName].terms) != -1) ) {
return colorTagHighlight;
} else {
return colorDefault;
}
}
function returnNodeFillStyle(node) {
if (new_original_traces[node.nodeName] && new_original_traces[node.nodeName].trace_type=="termination") {
return colorTermination;
} else if (new_original_traces[node.nodeName] && new_original_traces[node.nodeName].trace_type=="fulfillment") {
return colorFulfillment;
} else if (node.childNodes && node.childNodes.length < 1) {
return colorForgotten;
} else {
return colorContinuation;
}
}
function returnLineColor(node, link) {
if (link.targetNode.nodeValue && link.targetNode.nodeValue.highlight==1) {
return colorHighlight;
} else if (link.targetNode.parentNode && link.targetNode.parentNode.nodeValue ){
if (link.targetNode.parentNode.nodeValue.highlight==1) {
link.targetNode.nodeValue.highlight="highlighted-son";
} else {
link.targetNode.nodeValue.highlight = link.targetNode.parentNode.nodeValue.highlight;
}
if(link.targetNode.nodeValue.highlight=="highlighted-son") {
return colorHighlight;
} else {
return colorDefault;
}
} else {
return colorDefault;
}
}
function returnLineWidth(node, link) {
if ((link.targetNode.nodeValue && link.targetNode.nodeValue.selected==1) || link.targetNode.nodeValue && link.targetNode.nodeValue.highlight==1) {
return 3;
} else if ((link.targetNode.parentNode && link.targetNode.parentNode.nodeValue) || link.targetNode.parentNode && link.targetNode.parentNode.nodeValue) {
if (link.targetNode.parentNode.nodeValue.selected==1 || link.targetNode.parentNode.nodeValue.highlight==1) {
link.targetNode.nodeValue.selected="selection-son";
} else {
link.targetNode.nodeValue.selected = link.targetNode.parentNode.nodeValue.selected;
}
if (link.targetNode.nodeValue.selected=="selection-son" || link.targetNode.nodeValue.highlight=="highlighted-son") {
return 3;
} else {
return 1;
}
} else {
return 1;
}
}
// Add detailed information about a trace (specified by nodeName) to page
function updateTraceData(nodeName){
if (new_original_traces[nodeName]) {
var submission_link = $('').attr({'href': '?q=node/' + new_original_traces[nodeName].submission_parent_id}).html(new_original_traces[nodeName].submission_parent);
$('#submission_title_label').html(submission_link);
var assignment_link = $('').attr({'href': '?q=node/' + new_original_traces[nodeName].assignment_parent_id}).html(new_original_traces[nodeName].assignment_parent);
$('#assignment_title_label').html(assignment_link);
var artifact_link = $('').attr({'href': '?q=node/' + new_original_traces[nodeName].artifact_parent_id}).html(new_original_traces[nodeName].artifact_parent);
$('#artifact_title_label').html(artifact_link);
var trace_link = $('').attr({'href': '?q=node/' + new_original_traces[nodeName].trace_id}).html(new_original_traces[nodeName].title);
$('#trace_title_label').html(trace_link);
$('#trace_desc_label').text(new_original_traces[nodeName].description);
$('#trace_type_label').text(new_original_traces[nodeName].trace_type);
$('#tags_label').text(new_original_traces[nodeName].terms.toString());
} else {
$('#artifact_name_label').text("Undefined");
$('#trace_title_label').text("Undefined");
$('#trace_desc_label').text("Undefined");
$('#trace_type_label').text("Undefined");
$('#tags_label').text("Undefined");
}
$('#trace-data-holder').show();
}
// Tag Cloud Functions
function getTagFrequencyRange(new_tags) {
var frequencyRange = [new_tags[0].frequency, new_tags[0].frequency]
for (var key in new_tags) {
frequencyRange[0] = new_tags[key].frequency < frequencyRange[0] ? new_tags[key].frequency : frequencyRange[0];
frequencyRange[1] = new_tags[key].frequency > frequencyRange[1] ? new_tags[key].frequency : frequencyRange[1];
}
return frequencyRange;
}
function drawTagCloud(new_tags) {
for (var key in new_tags) {
var fontSize = getTagFontSize(new_tags[key].frequency);
var span = $('').addClass('tag').css({'font-size': fontSize}).html(new_tags[key].tag);
$(span).click(function(){
if (!$(this).hasClass('highlight')) {
$('.tag').removeClass('highlight');
var tag = $(this).html();
treeNodes.def('tag', tag);
treeNodes.render();
$(this).addClass('highlight');
} else {
treeNodes.def('tag', 'nan');
treeNodes.render();
$(this).removeClass('highlight');
}
});
$('#tag-cloud p').append(span, ' ');
}
}
function getTagFontSize(frequency) {
var fontSize = (frequency-frequencyRange[0])/(frequencyRange[1]-frequencyRange[0]) * (maxTagFontSize-minTagFontSize) + minTagFontSize;
fontSize += 'px';
return fontSize;
}
}
// Hierarchy Functions
function convertToHierarchy(list) {
// Discard duplicates and set up parent/child relationships
var children = {};
var hasParent = {};
for (var key in list) {
var item = list[key];
if (!children[key]) {
children[key] = {};
}
if (item.parent_traces != null && item.parent_traces.length > 0) {
// Find the first valid parent trace
for (var i in item.parent_traces) {
if (item.parent_traces[i] != null && !(item.parent_traces[i] instanceof Array)) {
if (!children[item.parent_traces[i]]) {
children[item.parent_traces[i]] = {}
}
children[item.parent_traces[i]][key] = true; /* dummy value */
hasParent[key] = true;
break;
}
}
}
}
// Now build the hierarchy
var result = {};
for (var key in children) {
if (!hasParent[key]) {
result[key] = buildNodeRecursive(key, children);
}
}
return result;
}
function buildNodeRecursive(key, children) {
var node = {};
for (var child in children[key]) {
node[child] = buildNodeRecursive(child, children);
}
return node;
}
function paddHierarchy(hierarchy, rings, group, data) {
// make an array of ring titles
var ring_array = [];
for (ring_key in rings) {
ring_array.push(rings[ring_key].id);
}
// traverse each item in the path
traverse(hierarchy, ring_array, group, data, 0);
// build padded hierarchy
var new_hierarchy = {};
for (var i = 0; i < structure.length; i++) {
var ring_no = structure[i][1];
var parent_no = structure[i][0];
var key_array = new Array();
key_array = key_array.concat(structure[i][2]);
var id = structure[i][3];
var ring_diff = ring_no - parent_no;
var key_string = key_array.join("']['");
var parent_string = '';
var new_key_array = [];
for (var j = 0; j < key_array.length - 1; j++) {
parent_string += "['" + key_array[j] + "']";
new_key_array.push(key_array[j])
}
key_string = "hierarchy['" + key_string + "']";
// if the difference between the ring number and the parent number is greater than 0
if (ring_diff > 0) {
var not_string = ''
// add that many placeholder rings in between
var padding = structure[i][2].length == 1 ? ring_diff : ring_diff -1;
for (var j = 0; j < padding; j++) {
not_string += "['" + i + '_' + j + "']";
// modify key_array to include the placeholders
new_key_array.push(i + '_' + j);
var padded_key_string = new_key_array.join("']['");
padded_key_string = "['" + padded_key_string + "']";
eval('hierarchy' + padded_key_string + " = {}");
}
// Put the object here
new_key_array.push(id);
var padded_key_string = new_key_array.join("']['");
padded_key_string = "['" + padded_key_string + "']";
eval('hierarchy' + padded_key_string + ' = ' + key_string);
// Update the objects children's keys
// for each row in the structure array starting with the next in the list to be processed
for (var j = i + 1; j < structure.length; j++) {
var change_key = 1;
var update = true;
// for each key in the parent's original key_array
for (var k = 0; k < structure[i][2].length; k++) {
update = true;
// compare the parent's key with this key, if they don't match, don't update this row
if (structure[i][2][k] != structure[j][2][k]) {
update = false;
break;
}
change_key++;
}
if (update == true) {
// remove the keys up to the parent's keys
structure[j][2].splice(0, structure[i][2].length);
// prepend the new padded key array
structure[j][2] = new_key_array.concat(structure[j][2]);
}
}
// Remove the original object
if ('hierarchy' + padded_key_string != key_string) {
eval('delete ' + key_string);
}
// change it's key location
structure[i][2] = new_key_array;
}
}
return hierarchy;
}
var structure = [];
//called with every property and it's value
function process(key, value, rings, group, orig_item, parent_no, key_array) {
if (!key_array) {
key_array = [];
}
var new_key_array = [];
new_key_array = new_key_array.concat(key_array);
new_key_array.push(key);
var ring_no = rings.indexOf(orig_item[group]);
structure.push([parent_no, ring_no, new_key_array, key]);
return [ring_no, new_key_array]
}
function traverse(o, rings, group, orig_data, parent_no, parent_array) {
for (var l in o) {
var child_data = process(l, o[l], rings, group, orig_data[l], parent_no, parent_array);
if (typeof(o[l]) == "object") {
traverse(o[l], rings, group, orig_data, child_data[0], child_data[1]);
}
}
}