/* * 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]); } } }