-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgraph.js
More file actions
258 lines (238 loc) · 7.43 KB
/
graph.js
File metadata and controls
258 lines (238 loc) · 7.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
import * as d3 from 'd3';
/**
* Get the opposite edge end name
* @param {string} edgeEnd - edge end, either source or target
* @returns other edge end
*/
function getOtherEdgeEnd(edgeEnd) {
return edgeEnd === 'target' ? 'source' : 'target';
}
/**
* Get angle of line connecting points, in radians
* @param {int} x1 x of first point
* @param {int} y1 y of first point
* @param {int} x2 x of second point
* @param {int} y2 y of second point
*/
function getAngle(x1, y1, x2, y2) {
const delta_x = x2 - x1;
const delta_y = y2 - y1;
const theta_radians = Math.atan2(delta_y, delta_x);
return theta_radians;
}
/**
* Calculate the x and y of both edge ends as well as the quadratic curve point
* to make the edge curve
* @param {int} sourceX x of source node
* @param {int} sourceY y of source node
* @param {int} targetX x of target node
* @param {int} targetY y of target node
* @param {int} numEdges number of total edges
* @param {int} index index of edge to find its curve position
* @param {int} nodeRadius node radius
* @returns {obj} all the necessary points to make a curvy edge
*/
function getCurvedEdgePos(sourceX, sourceY, targetX, targetY, numEdges, index, nodeRadius) {
const arcWidth = Math.PI / 3;
const edgeStep = arcWidth / 5;
// get angle between nodes
const theta = getAngle(sourceX, sourceY, targetX, targetY);
// get adjusted angle based on edge index
const arc_p1 = theta + (edgeStep * index);
const arc_p2 = theta + Math.PI - (edgeStep * index);
// compute x's and y's
const x1 = sourceX + Math.cos(arc_p1) * nodeRadius;
const y1 = sourceY + Math.sin(arc_p1) * nodeRadius;
const x2 = targetX + Math.cos(arc_p2) * nodeRadius;
const y2 = targetY + Math.sin(arc_p2) * nodeRadius;
const alpha = 50; // tuning param
let l = (index * 0.1) + (index * 0.3); // num of edge
// move first arced edges out just a smidge
if (index === 1 || index === -1) {
l += (index * 0.4);
}
const bx = ((x1 + x2) / 2);
const by = (y1 + y2) / 2;
const vx = x2 - x1;
const vy = y2 - y1;
const norm_v = Math.sqrt(vx ** 2 + vy ** 2);
const vx_norm = vx / norm_v;
const vy_norm = vy / norm_v;
const vx_perp = -vy_norm;
const vy_perp = vx_norm;
const qx = bx + alpha * l * vx_perp;
const qy = by + alpha * l * vy_perp;
return {
x1, y1, qx, qy, x2, y2,
};
}
/**
* Get x and y positions of shortened line end
*
* Given a line, we will find the new line end according
* to the line angle and offset
* @param {int} x1 - line start x
* @param {int} y1 - line start y
* @param {int} x2 - line end x
* @param {int} y2 - line end y
* @param {int} offset - offset for current line end
* @returns {obj} x and y postions of new line end
*/
function getShortenedLineEnd(x1, y1, x2, y2, offset) {
const angle = getAngle(x1, y1, x2, y2);
const x = x1 + Math.cos(angle) * offset;
const y = y1 + Math.sin(angle) * offset;
return { x, y };
}
/**
* Find bounded value within a lower and upper bound
* @param {int} value - value to bound
* @param {int} upperBound - upper bound of value
* @param {int} lowerBound - lower bound of value; default: 0
* @returns {int} bounded value
*/
function getBoundedValue(value, upperBound, lowerBound = 0) {
return Math.max(lowerBound, Math.min(value, upperBound));
}
function fitTextWithEllipsis(text, el, nodeRadius, fontSize, dy) {
// Set up the SVG and circle
const svg = d3.select('body')
.append('svg')
.attr('width', 300)
.attr('height', 300);
const tempText = svg.append('text')
.attr('font-size', fontSize)
.style('visibility', 'hidden')// Hide temp text
.text(text);
const targetLength = nodeRadius * 2 * 0.9;
let finalText = text;
let textLength = tempText.node().getComputedTextLength();
if (textLength > nodeRadius * 2 * 1.25) {
while (textLength > targetLength && finalText.length > 0) {
finalText = finalText.slice(0, -1);
tempText.text(`${finalText.slice(0, -1)}...`);
textLength = tempText.node().getComputedTextLength();
}
finalText = `${finalText}...`;
}
tempText.remove();
svg.remove();
el.append('tspan')
.attr('x', 0)
.attr('dy', dy)
.text(finalText);
}
function fitTextIntoCircle() {
const el = d3.select(this);
const textLength = el.node().getComputedTextLength();
const text = el.text();
// grab the parent g tag
const parent = el.node().parentNode;
// grab the corresponding circle
const circle = d3.select(parent)
.select('circle');
// get circle radius
const nodeRadius = circle.attr('r');
const maxFontSize = nodeRadius * 0.4;
const minFontSize = 9;
const diameter = nodeRadius * 2;
const fontSize = `${Math.max(minFontSize, Math.min(maxFontSize, diameter / Math.sqrt(textLength)))}px`;
el.style('font-size', fontSize);
el.text('');
const words = text.split(' ');
if (words.length === 1 || textLength < 10) {
fitTextWithEllipsis(text, el, nodeRadius, fontSize, '0em');
} else {
// Split into two lines
const middle = Math.ceil(words.length / 2);
const firstLine = words.slice(0, middle).join(' ');
const secondLine = words.slice(middle).join(' ');
fitTextWithEllipsis(firstLine, el, nodeRadius, fontSize, '-0.4em');
fitTextWithEllipsis(secondLine, el, nodeRadius, fontSize, '1.2em');
}
}
/**
* Trim and add an ellipsis to the end of long node labels
*/
function ellipsisOverflow() {
const el = d3.select(this);
let textLength = el.node().getComputedTextLength();
let text = el.text();
// grab the parent g tag
const parent = el.node().parentNode;
// grab the corresponding circle
const circle = d3.select(parent)
.select('circle');
// get circle radius
const nodeRadius = circle.attr('r');
const diameter = nodeRadius * 2;
// give the text a little padding
const targetLength = diameter * 0.9;
while (textLength > targetLength && text.length > 0) {
text = text.slice(0, -1);
el.text(`${text}...`);
textLength = el.node().getComputedTextLength();
}
}
/**
* Get the middle of two points
* @param {obj} edge edge object
* @returns {obj} the x and y of the middle of two points
*/
function getEdgeMidpoint(edge) {
const { source, target } = edge;
const midX = (source.x + target.x) / 2;
const midY = (source.y + target.y) / 2;
return { x: midX, y: midY };
}
/**
* Determine if point is within a circle
* @param {int} x x position of point
* @param {int} y y position of point
* @param {int} cx center x of circle
* @param {int} cy center y of circle
* @param {int} r radius of circle
* @returns {bool} Is point within circle
*/
function isInside(x, y, cx, cy, r) {
return (x - cx) ** 2 + (y - cy) ** 2 <= r ** 2;
}
/**
* Should an edge have an arrow
* @param {obj} edge edge object
* @returns {str} url(#arrow) or empty string
*/
function shouldShowArrow(edge, symmetric_predicates = ['biolink:related_to']) {
return (edge.predicates && edge.predicates.findIndex((p) => !symmetric_predicates.includes(p)) > -1) || (edge.predicate && !symmetric_predicates.includes(edge.predicate));
}
/**
* Fade a DOM element in to view
*/
function showElement() {
d3.select(this)
.transition()
.duration(500)
.style('opacity', 1);
}
/**
* Fade a DOM element out of view
*/
function hideElement() {
d3.select(this)
.transition()
.duration(1000)
.style('opacity', 0);
}
export default {
getOtherEdgeEnd,
getCurvedEdgePos,
getShortenedLineEnd,
getBoundedValue,
ellipsisOverflow,
getEdgeMidpoint,
fitTextIntoCircle,
isInside,
shouldShowArrow,
showElement,
hideElement,
};