Skip to content

Commit 75d848a

Browse files
authored
Merge pull request #17 from CIDARLAB/condense-visualization
Condense visualization
2 parents 4fdaa77 + d425644 commit 75d848a

File tree

3 files changed

+195
-43
lines changed

3 files changed

+195
-43
lines changed

src/main/resources/static/css/knox.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -500,8 +500,10 @@ section {
500500

501501
.link {
502502
stroke: #999;
503-
stroke-opacity: .5;
504-
marker-end: url(#endArrow);
503+
}
504+
505+
.dashed-link {
506+
stroke-dasharray: 5;
505507
}
506508

507509
.node {

src/main/resources/static/js/knox.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,79 @@ window.onresize = function(e) {
7373
/*********************
7474
* HELPER FUNCTIONS
7575
*********************/
76+
/**
77+
* Determine and add 'show', 'optional', and 'reverseOrient' flags to each link
78+
* @param graph design space graph
79+
*/
80+
export function condenseVisualization(graph){
81+
let sourceTargetMap = {};
82+
83+
for(let i=0; i<graph.links.length; i++) {
84+
// add optional flag to all links
85+
graph.links[i].optional = false; //optional links show dashed lines
86+
graph.links[i].show = true; //will not be rendered if false
87+
graph.links[i].hasReverseOrient = false;
88+
89+
//get all source/target pairs
90+
let sourceNode = graph.links[i].source.toString();
91+
let targetNode = graph.links[i].target.toString();
92+
let stPairNum = sourceNode + targetNode;
93+
94+
if(!(stPairNum in sourceTargetMap)){
95+
sourceTargetMap[stPairNum] = i; //save index
96+
}
97+
else{
98+
let dupLink1 = graph.links[sourceTargetMap[stPairNum]];
99+
let dupLink2 = graph.links[i];
100+
101+
if(dupLink1.componentIDs.length && dupLink2.componentIDs.length){
102+
103+
//check ID equality
104+
let sortedComponentIDs1 = dupLink1.componentIDs.sort();
105+
let sortedComponentIDs2 = dupLink2.componentIDs.sort();
106+
if(sortedComponentIDs1.length !== sortedComponentIDs2.length ||
107+
sortedComponentIDs1.every(function(value, index) {
108+
return value !== sortedComponentIDs2[index]
109+
})){
110+
continue;
111+
}
112+
113+
//check role equality
114+
let sortedRoles1 = dupLink1.componentRoles.sort();
115+
let sortedRoles2 = dupLink2.componentRoles.sort();
116+
if(sortedRoles1.length !== sortedRoles2.length ||
117+
sortedRoles1.every(function(value, index) {
118+
return value !== sortedRoles2[index]
119+
})){
120+
continue;
121+
}
122+
123+
// check orientation
124+
if(dupLink1.orientation === "INLINE"){
125+
dupLink1.hasReverseOrient = true;
126+
dupLink2.hasReverseOrient = true;
127+
dupLink2.show = false;
128+
} else {
129+
dupLink1.hasReverseOrient = true;
130+
dupLink2.hasReverseOrient = true;
131+
dupLink1.show = false;
132+
}
133+
}
134+
135+
else if(dupLink1.componentIDs.length){
136+
dupLink1.optional = true;
137+
dupLink2.show = false;
138+
} else {
139+
dupLink2.optional = true;
140+
dupLink1.show = false;
141+
}
142+
143+
if(dupLink1.orientation === 'NONE'){
144+
sourceTargetMap[stPairNum] = i; //save new index
145+
}
146+
}
147+
}
148+
}
76149

77150
// Utility for disabling navigation features.
78151
// Exposes the function disableTabs.

src/main/resources/static/js/target.js

Lines changed: 118 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
2-
import {knoxClass, getSBOLImage, splitElementID} from "./knox.js";
1+
import {knoxClass, getSBOLImage, splitElementID, condenseVisualization} from "./knox.js";
32

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

3635
setGraph(graph) {
36+
condenseVisualization(graph);
37+
3738
var zoom = d3.behavior.zoom()
3839
.scaleExtent([1, 10])
3940
.on("zoom", () => {
4041
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
4142
});
4243

43-
var svg = d3.select(this.id).call(zoom).append("svg:g");
44-
svg.append("defs").append("marker")
45-
.attr("id", "endArrow")
46-
.attr("viewBox", "0 -5 10 10")
47-
.attr("refX", 6)
48-
.attr("markerWidth", 6)
49-
.attr("markerHeight", 6)
50-
.attr("orient", "auto")
51-
.append("path")
52-
.attr("d", "M0,-5L10,0L0,5")
53-
.attr("fill", "#999")
54-
.attr("opacity", "0.5");
55-
var force = (this.layout = d3.layout.force());
56-
force.drag().on("dragstart", () => {
57-
d3.event.sourceEvent.stopPropagation();
58-
});
59-
force.charge(-400).linkDistance(100);
60-
force.nodes(graph.nodes).links(graph.links).size([
61-
$(this.id).parent().width(), $(this.id).parent().height()
62-
]).start();
63-
64-
var linksEnter = svg.selectAll(".link")
65-
.data(graph.links)
66-
.enter();
67-
68-
var links = linksEnter.append("path")
69-
.attr("class", "link");
44+
//add SVG container
45+
let svg = d3.select(this.id)
46+
.call(zoom)
47+
.append("svg:g");
48+
49+
//def objects are not displayed until referenced
50+
let defs = svg.append("svg:defs");
51+
52+
let force = (this.layout = d3.layout.force())
53+
.charge(-400)
54+
.linkDistance(100)
55+
.nodes(graph.nodes)
56+
.links(graph.links)
57+
.size([$(this.id).parent().width(), $(this.id).parent().height()])
58+
.start();
59+
force.drag().on("dragstart", () => {
60+
d3.event.sourceEvent.stopPropagation();
61+
});
7062

71-
var nodesEnter = svg.selectAll(".node")
63+
// add nodes (circles)
64+
let nodesEnter = svg.selectAll(".node")
7265
.data(graph.nodes)
7366
.enter();
7467

75-
var circles = nodesEnter.append("circle")
68+
let circles = nodesEnter.append("circle")
7669
.attr("class", function(d) {
7770
if (d.nodeTypes.length === 0) {
7871
return "node";
@@ -82,14 +75,60 @@ export default class Target{
8275
return "accept-node";
8376
}
8477
})
85-
.attr("r", 7).call(force.drag);
78+
.attr("r", 7) //radius
79+
.call(force.drag);
8680

87-
const sbolImgSize = 30;
81+
// Filter out links if the "show" flag is false
82+
let linksEnter = svg.selectAll(".link")
83+
.data(graph.links.filter(link => link.show))
84+
.enter();
85+
86+
function marker(isBlank) {
87+
88+
let fill = isBlank? "none": "#999";
89+
let id = "arrow"+fill.replace("#", "");
90+
91+
defs.append("svg:marker")
92+
.attr("id", id)
93+
.attr("viewBox", "0 -5 10 10")
94+
.attr("refX", 6)
95+
.attr("markerWidth", 6)
96+
.attr("markerHeight", 6)
97+
.attr("orient", "auto")
98+
.append("svg:path")
99+
.attr("d", "M0,-5L10,0L0,5")
100+
.attr("stroke", "#999")
101+
.attr("fill", fill);
102+
103+
return "url(#" + id + ")";
104+
}
105+
106+
// Optional links will be rendered as dashed lines
107+
// Blank edges will be rendered with an unfilled arrow
108+
let links = linksEnter.append("path")
109+
.attr("class", (l) => {
110+
if (l.optional){
111+
return "link dashed-link";
112+
}
113+
return "link"
114+
})
115+
.attr("d", "M0,-5L10,0L0,5")
116+
.attr("marker-end",(l) => {
117+
let isBlank = l["componentRoles"].length === 0 && l["componentIDs"].length === 0;
118+
return marker(isBlank);
119+
});
88120

121+
//place SBOL svg on links
122+
const sbolImgSize = 30;
89123
let images = linksEnter.append("svg:image")
90124
.attr("height", sbolImgSize)
91125
.attr("width", sbolImgSize)
92-
.attr("class", "sboltip")
126+
.attr("class", (d) => {
127+
if (d.hasOwnProperty("componentRoles") && d["componentRoles"].length > 0) {
128+
return "sboltip";
129+
}
130+
return null;
131+
})
93132
.attr("title", (d) => {
94133
if (d.hasOwnProperty("componentIDs")) {
95134
let titleStr = "";
@@ -104,21 +143,34 @@ export default class Target{
104143
return titleStr;
105144
}
106145
})
107-
.attr("xlink:href", (d) => {
146+
.attr("href", (d) => {
147+
if (d.hasOwnProperty("componentRoles") && d["componentRoles"].length > 0) {
148+
return getSBOLImage(d["componentRoles"][0]);
149+
}
150+
return null;
151+
});
152+
153+
let reverseImgs = linksEnter.append("svg:image")
154+
.attr("height", sbolImgSize)
155+
.attr("width", sbolImgSize)
156+
.attr("href", (d) => {
108157
if (d.hasOwnProperty("componentRoles")) {
109-
if (d["componentRoles"].length > 0) {
110-
let role = d["componentRoles"][0];
111-
return getSBOLImage(role);
158+
if (d["componentRoles"].length > 0 && d.hasReverseOrient) {
159+
return getSBOLImage(d["componentRoles"][0]);
112160
}
113161
}
114-
return "";
115-
});
162+
return null;
163+
});
116164

165+
//place tooltip on the SVG images
117166
$('.sboltip').tooltipster({
118167
theme: 'tooltipster-shadow'
119168
});
120169

170+
// Handles positioning when moved
121171
force.on("tick", function () {
172+
173+
// Position links
122174
links.attr('d', function(d) {
123175
var deltaX = d.target.x - d.source.x,
124176
deltaY = d.target.y - d.source.y,
@@ -134,18 +186,43 @@ export default class Target{
134186
return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
135187
});
136188

189+
// Position circles
137190
circles.attr("cx", function (d) {
138191
return d.x;
139192
})
140193
.attr("cy", function (d) {
141194
return d.y;
142195
});
143196

197+
// Position SBOL images
144198
images.attr("x", function (d) {
145-
return (d.source.x + d.target.x) / 2 - sbolImgSize / 2;
199+
if(d.hasReverseOrient){
200+
return (d.source.x + d.target.x) / 2 - sbolImgSize;
201+
}
202+
return (d.source.x + d.target.x) / 2 - sbolImgSize / 2;
203+
})
204+
.attr("y", function (d) {
205+
return (d.source.y + d.target.y) / 2 - sbolImgSize / 2;
206+
})
207+
.attr('transform',function(d){
208+
//transform 180 if the orientation is REVERSE_COMPLEMENT
209+
if(d.orientation === "REVERSE_COMPLEMENT" && !d.hasReverseOrient){
210+
let x1 = (d.source.x + d.target.x) / 2; //the center x about which you want to rotate
211+
let y1 = (d.source.y + d.target.y) / 2; //the center y about which you want to rotate
212+
return `rotate(180, ${x1}, ${y1})`;
213+
}
214+
});
215+
216+
reverseImgs.attr("x", function (d) {
217+
return (d.source.x + d.target.x) / 2 - sbolImgSize;
146218
})
147219
.attr("y", function (d) {
148220
return (d.source.y + d.target.y) / 2 - sbolImgSize / 2;
221+
})
222+
.attr('transform',function(d){
223+
let x1 = (d.source.x + d.target.x) / 2; //the center x about which you want to rotate
224+
let y1 = (d.source.y + d.target.y) / 2; //the center y about which you want to rotate
225+
return `rotate(180, ${x1}, ${y1})`;
149226
});
150227
});
151228
}

0 commit comments

Comments
 (0)