Skip to content

Commit

Permalink
Merge pull request #17 from CIDARLAB/condense-visualization
Browse files Browse the repository at this point in the history
Condense visualization
  • Loading branch information
nroehner authored Jun 28, 2019
2 parents 4fdaa77 + d425644 commit 75d848a
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 43 deletions.
6 changes: 4 additions & 2 deletions src/main/resources/static/css/knox.css
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,10 @@ section {

.link {
stroke: #999;
stroke-opacity: .5;
marker-end: url(#endArrow);
}

.dashed-link {
stroke-dasharray: 5;
}

.node {
Expand Down
73 changes: 73 additions & 0 deletions src/main/resources/static/js/knox.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,79 @@ window.onresize = function(e) {
/*********************
* HELPER FUNCTIONS
*********************/
/**
* Determine and add 'show', 'optional', and 'reverseOrient' flags to each link
* @param graph design space graph
*/
export function condenseVisualization(graph){
let sourceTargetMap = {};

for(let i=0; i<graph.links.length; i++) {
// add optional flag to all links
graph.links[i].optional = false; //optional links show dashed lines
graph.links[i].show = true; //will not be rendered if false
graph.links[i].hasReverseOrient = false;

//get all source/target pairs
let sourceNode = graph.links[i].source.toString();
let targetNode = graph.links[i].target.toString();
let stPairNum = sourceNode + targetNode;

if(!(stPairNum in sourceTargetMap)){
sourceTargetMap[stPairNum] = i; //save index
}
else{
let dupLink1 = graph.links[sourceTargetMap[stPairNum]];
let dupLink2 = graph.links[i];

if(dupLink1.componentIDs.length && dupLink2.componentIDs.length){

//check ID equality
let sortedComponentIDs1 = dupLink1.componentIDs.sort();
let sortedComponentIDs2 = dupLink2.componentIDs.sort();
if(sortedComponentIDs1.length !== sortedComponentIDs2.length ||
sortedComponentIDs1.every(function(value, index) {
return value !== sortedComponentIDs2[index]
})){
continue;
}

//check role equality
let sortedRoles1 = dupLink1.componentRoles.sort();
let sortedRoles2 = dupLink2.componentRoles.sort();
if(sortedRoles1.length !== sortedRoles2.length ||
sortedRoles1.every(function(value, index) {
return value !== sortedRoles2[index]
})){
continue;
}

// check orientation
if(dupLink1.orientation === "INLINE"){
dupLink1.hasReverseOrient = true;
dupLink2.hasReverseOrient = true;
dupLink2.show = false;
} else {
dupLink1.hasReverseOrient = true;
dupLink2.hasReverseOrient = true;
dupLink1.show = false;
}
}

else if(dupLink1.componentIDs.length){
dupLink1.optional = true;
dupLink2.show = false;
} else {
dupLink2.optional = true;
dupLink1.show = false;
}

if(dupLink1.orientation === 'NONE'){
sourceTargetMap[stPairNum] = i; //save new index
}
}
}
}

// Utility for disabling navigation features.
// Exposes the function disableTabs.
Expand Down
159 changes: 118 additions & 41 deletions src/main/resources/static/js/target.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@

import {knoxClass, getSBOLImage, splitElementID} from "./knox.js";
import {knoxClass, getSBOLImage, splitElementID, condenseVisualization} from "./knox.js";

// The target class observes an SVG element on the page, and
// provides methods for setting and clearing graph data. A variable
Expand Down Expand Up @@ -34,45 +33,39 @@ export default class Target{
}

setGraph(graph) {
condenseVisualization(graph);

var zoom = d3.behavior.zoom()
.scaleExtent([1, 10])
.on("zoom", () => {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
});

var svg = d3.select(this.id).call(zoom).append("svg:g");
svg.append("defs").append("marker")
.attr("id", "endArrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 6)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", "#999")
.attr("opacity", "0.5");
var force = (this.layout = d3.layout.force());
force.drag().on("dragstart", () => {
d3.event.sourceEvent.stopPropagation();
});
force.charge(-400).linkDistance(100);
force.nodes(graph.nodes).links(graph.links).size([
$(this.id).parent().width(), $(this.id).parent().height()
]).start();

var linksEnter = svg.selectAll(".link")
.data(graph.links)
.enter();

var links = linksEnter.append("path")
.attr("class", "link");
//add SVG container
let svg = d3.select(this.id)
.call(zoom)
.append("svg:g");

//def objects are not displayed until referenced
let defs = svg.append("svg:defs");

let force = (this.layout = d3.layout.force())
.charge(-400)
.linkDistance(100)
.nodes(graph.nodes)
.links(graph.links)
.size([$(this.id).parent().width(), $(this.id).parent().height()])
.start();
force.drag().on("dragstart", () => {
d3.event.sourceEvent.stopPropagation();
});

var nodesEnter = svg.selectAll(".node")
// add nodes (circles)
let nodesEnter = svg.selectAll(".node")
.data(graph.nodes)
.enter();

var circles = nodesEnter.append("circle")
let circles = nodesEnter.append("circle")
.attr("class", function(d) {
if (d.nodeTypes.length === 0) {
return "node";
Expand All @@ -82,14 +75,60 @@ export default class Target{
return "accept-node";
}
})
.attr("r", 7).call(force.drag);
.attr("r", 7) //radius
.call(force.drag);

const sbolImgSize = 30;
// Filter out links if the "show" flag is false
let linksEnter = svg.selectAll(".link")
.data(graph.links.filter(link => link.show))
.enter();

function marker(isBlank) {

let fill = isBlank? "none": "#999";
let id = "arrow"+fill.replace("#", "");

defs.append("svg:marker")
.attr("id", id)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 6)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5")
.attr("stroke", "#999")
.attr("fill", fill);

return "url(#" + id + ")";
}

// Optional links will be rendered as dashed lines
// Blank edges will be rendered with an unfilled arrow
let links = linksEnter.append("path")
.attr("class", (l) => {
if (l.optional){
return "link dashed-link";
}
return "link"
})
.attr("d", "M0,-5L10,0L0,5")
.attr("marker-end",(l) => {
let isBlank = l["componentRoles"].length === 0 && l["componentIDs"].length === 0;
return marker(isBlank);
});

//place SBOL svg on links
const sbolImgSize = 30;
let images = linksEnter.append("svg:image")
.attr("height", sbolImgSize)
.attr("width", sbolImgSize)
.attr("class", "sboltip")
.attr("class", (d) => {
if (d.hasOwnProperty("componentRoles") && d["componentRoles"].length > 0) {
return "sboltip";
}
return null;
})
.attr("title", (d) => {
if (d.hasOwnProperty("componentIDs")) {
let titleStr = "";
Expand All @@ -104,21 +143,34 @@ export default class Target{
return titleStr;
}
})
.attr("xlink:href", (d) => {
.attr("href", (d) => {
if (d.hasOwnProperty("componentRoles") && d["componentRoles"].length > 0) {
return getSBOLImage(d["componentRoles"][0]);
}
return null;
});

let reverseImgs = linksEnter.append("svg:image")
.attr("height", sbolImgSize)
.attr("width", sbolImgSize)
.attr("href", (d) => {
if (d.hasOwnProperty("componentRoles")) {
if (d["componentRoles"].length > 0) {
let role = d["componentRoles"][0];
return getSBOLImage(role);
if (d["componentRoles"].length > 0 && d.hasReverseOrient) {
return getSBOLImage(d["componentRoles"][0]);
}
}
return "";
});
return null;
});

//place tooltip on the SVG images
$('.sboltip').tooltipster({
theme: 'tooltipster-shadow'
});

// Handles positioning when moved
force.on("tick", function () {

// Position links
links.attr('d', function(d) {
var deltaX = d.target.x - d.source.x,
deltaY = d.target.y - d.source.y,
Expand All @@ -134,18 +186,43 @@ export default class Target{
return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
});

// Position circles
circles.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});

// Position SBOL images
images.attr("x", function (d) {
return (d.source.x + d.target.x) / 2 - sbolImgSize / 2;
if(d.hasReverseOrient){
return (d.source.x + d.target.x) / 2 - sbolImgSize;
}
return (d.source.x + d.target.x) / 2 - sbolImgSize / 2;
})
.attr("y", function (d) {
return (d.source.y + d.target.y) / 2 - sbolImgSize / 2;
})
.attr('transform',function(d){
//transform 180 if the orientation is REVERSE_COMPLEMENT
if(d.orientation === "REVERSE_COMPLEMENT" && !d.hasReverseOrient){
let x1 = (d.source.x + d.target.x) / 2; //the center x about which you want to rotate
let y1 = (d.source.y + d.target.y) / 2; //the center y about which you want to rotate
return `rotate(180, ${x1}, ${y1})`;
}
});

reverseImgs.attr("x", function (d) {
return (d.source.x + d.target.x) / 2 - sbolImgSize;
})
.attr("y", function (d) {
return (d.source.y + d.target.y) / 2 - sbolImgSize / 2;
})
.attr('transform',function(d){
let x1 = (d.source.x + d.target.x) / 2; //the center x about which you want to rotate
let y1 = (d.source.y + d.target.y) / 2; //the center y about which you want to rotate
return `rotate(180, ${x1}, ${y1})`;
});
});
}
Expand Down

0 comments on commit 75d848a

Please sign in to comment.