1 | |
---|
2 | |
---|
3 | var item_li = null; |
---|
4 | |
---|
5 | var svg = null; // main svg-element |
---|
6 | var css; |
---|
7 | var data_all = null; // global holder for all (input) data |
---|
8 | var nodes_sel = []; // global holder for selected data (selected nodes) |
---|
9 | var data_show = null; // global holder for data to show closure over nodes_sel |
---|
10 | var nest = {}; |
---|
11 | var detail_data = null; // global holder for detail-data (in html) |
---|
12 | |
---|
13 | |
---|
14 | var input_prefix = "input-"; |
---|
15 | var select_rect_min_size = 5; |
---|
16 | var first_level_margin = 20; |
---|
17 | var min_circle = 4; |
---|
18 | var max_circle = 80; |
---|
19 | |
---|
20 | var show_count = 1; |
---|
21 | var show_arrows = 0; |
---|
22 | |
---|
23 | var comp_reg_url = "http://catalog.clarin.eu/ds/ComponentRegistry/?item="; |
---|
24 | var mdrepo_url_search = "http://localhost:8680/exist/apps/cr-xq/mdrepo/index.html?operation=searchRetrieve&x-context=&query="; |
---|
25 | /*http://localhost:8680/exist/apps/cr-xq/mdrepo/fcs?operation=scan&scanClause=cmd:CountryName&x-context=&x-format=htmlpage*/ |
---|
26 | var mdrepo_url_scan = "http://localhost:8680/exist/apps/cr-xq/mdrepo/fcs?operation=scan&x-context=&x-format=htmlpage&sort=size&scanClause="; |
---|
27 | /*var source_file = "../scripts/cmd-dep-graph-d3_all_svg.json"*/ |
---|
28 | /*var source_file = "file:/C:/Users/m/3/clarin/_repo/SMC/output/cmd-dep-graph.d3.js" |
---|
29 | var detail_file = "file:/C:/Users/m/3/clarin/_repo/SMC/output/smc_stats_detail.html" |
---|
30 | */ |
---|
31 | /* |
---|
32 | var source_file = "/smc/_structure-graph.json"; |
---|
33 | var source_file = "cmd-dep-graph.d3.js"; |
---|
34 | var detail_file = "smc_stats_detail.html"; |
---|
35 | var userdocs_file = "userdocs.html"; |
---|
36 | */ |
---|
37 | //now graph-param is used |
---|
38 | //var source_file = "/smc/smc-graph.d3"; |
---|
39 | /*var source_file = "/smc/cmd-dep-graph.d3.js";*/ |
---|
40 | var detail_file = "smc_stats_detail.html"; |
---|
41 | var userdocs_file = "userdocs.html"; |
---|
42 | |
---|
43 | var opts = {"graph": {"value":"/smc/data/profiles.js", |
---|
44 | "values":[{value: "/smc/data/smc-graph-basic.js", label:"SMC graph basic"}, |
---|
45 | {value: "/smc/data/smc-graph-all.js", label:"SMC graph all"}, |
---|
46 | {value: "/smc/data/smc-graph-profiles-datcats.js", label:"only profiles + datcats"}, |
---|
47 | {value: "/smc/data/smc-graph-groups-profiles-datcats-rr.js", label:"profiles+datcats+groups+rr"}, |
---|
48 | {value: "/smc/data/profiles.js", label:"just profiles"} |
---|
49 | |
---|
50 | /*, |
---|
51 | {value: "/smc/data/smc-graph-mdrepo-stats.js", label:"instance data"}*/ |
---|
52 | |
---|
53 | ], "widget":"selectone" }, |
---|
54 | "depth-before": {"value":2, "min":0, "max":10, "widget":"slider"}, |
---|
55 | "depth-after":{"value":2, "min":0, "max":10, "widget":"slider"}, |
---|
56 | "link-distance": {"value":120, "min":10, "max":300, "widget":"slider" }, |
---|
57 | "charge":{"value":250, "min":10, "max":1000, "widget":"slider" }, |
---|
58 | "friction":{"value":75, "min":1, "max":100, "widget":"slider" }, |
---|
59 | "node-size": {"value":"count", "values":["1","4","8","16","count"], "widget":"selectone" }, |
---|
60 | "labels": {"value":"show", "values":["show","hide"], "widget":"selectone" }, |
---|
61 | "curve": {"value":"straight", "values":["straight","arc"], "widget":"selectone" }, |
---|
62 | "layout": {"value":"force", "values":["vertical-tree", "horizontal-tree", "weak-tree","force","dot", "freeze"], "widget":"selectone" }, |
---|
63 | "selected": {"widget":"hidden" }, |
---|
64 | "link": {"widget":"link", "label":""}, |
---|
65 | "download": {"widget":"link", "label":""}, |
---|
66 | "add_profile": {"widget":"link", "label":"Add profile", "widget":""} |
---|
67 | }; |
---|
68 | |
---|
69 | |
---|
70 | /** temporary helper function |
---|
71 | to easily get the param-data |
---|
72 | */ |
---|
73 | function opt(key) { |
---|
74 | var val = $("#navigate").data("qi").getParamValue(key); |
---|
75 | return typeof val == 'undefined' ? "" : val; |
---|
76 | } |
---|
77 | |
---|
78 | /** gets the data for the graph and calls rendering of the lists |
---|
79 | * @name initGraph |
---|
80 | * @function |
---|
81 | */ |
---|
82 | function initGraph (graph_source) |
---|
83 | { |
---|
84 | |
---|
85 | // load data |
---|
86 | d3.json(graph_source, |
---|
87 | function(json) { |
---|
88 | // return if data missing |
---|
89 | if (json==null) { notifyUser("source data missing: " + graph_source ); return null} |
---|
90 | data_all = json; |
---|
91 | data_all.links.forEach(function(d) { |
---|
92 | //resolve numeric index to node references |
---|
93 | src_ix = d.source; |
---|
94 | d.source = data_all.nodes[src_ix]; |
---|
95 | d.source.index = src_ix; |
---|
96 | trg_ix = d.target; |
---|
97 | d.target = data_all.nodes[trg_ix]; |
---|
98 | d.target.index = trg_ix; |
---|
99 | src_key = d.source.key; |
---|
100 | trg_key = d.target.key; |
---|
101 | }); |
---|
102 | // generate lookup hashes for neighbours; |
---|
103 | add_lookups(data_all); |
---|
104 | |
---|
105 | // get min/max on some properties |
---|
106 | var init_x_arr = []; |
---|
107 | data_all.nodes.forEach(function(d,i){init_x_arr.push(d.init_x);}) |
---|
108 | data_all.init_x_min = d3.min(init_x_arr); |
---|
109 | data_all.init_x_max = d3.max(init_x_arr); |
---|
110 | |
---|
111 | var init_level = []; |
---|
112 | data_all.nodes.forEach(function(d,i){init_level.push(+d.level);}) |
---|
113 | data_all.level_min = d3.min(init_level); |
---|
114 | |
---|
115 | var init_count = []; |
---|
116 | data_all.nodes.forEach(function(d,i){init_count.push(+d.count);}) |
---|
117 | |
---|
118 | data_all.count_max = d3.max(init_count); |
---|
119 | data_all.node_size_ratio = Math.sqrt(data_all.count_max) / max_circle; |
---|
120 | |
---|
121 | notifyUser("count max: " + data_all.count_max + "; " |
---|
122 | + "node_size_ratio: " + data_all.node_size_ratio); |
---|
123 | |
---|
124 | // should be delivered by the data directly |
---|
125 | data_all.nodes.forEach(function(d,i) { |
---|
126 | d.x = d.init_x; |
---|
127 | d.y = d.init_y; |
---|
128 | }); |
---|
129 | |
---|
130 | // get selected nodes (if any) from param |
---|
131 | selected_ids = opt("selected").split(","); |
---|
132 | selectNodeByKey(selected_ids); |
---|
133 | /*var selected_match = 0; |
---|
134 | for (var i = 0; i < selected_ids.length; i++) |
---|
135 | { if (data_all.nodes_index[selected_ids[i]]) { |
---|
136 | data_all.nodes_index[selected_ids[i]].selected = 1; |
---|
137 | selected_match ++; |
---|
138 | } |
---|
139 | } |
---|
140 | // if something was selected, update and render Graph and Detail |
---|
141 | if (selected_match) { updateSelected();} |
---|
142 | */ |
---|
143 | renderIndex(); |
---|
144 | |
---|
145 | |
---|
146 | }); |
---|
147 | } |
---|
148 | |
---|
149 | /** put grouped list of nodes into the target container*/ |
---|
150 | function renderIndex (data, target_container_selector) { |
---|
151 | data = typeof data !== 'undefined' ? data : data_all.nodes; |
---|
152 | target_container_selector = typeof target_container_selector !== 'undefined' ? target_container_selector : index_container_selector; |
---|
153 | renderNodeList (data, target_container_selector); |
---|
154 | } |
---|
155 | |
---|
156 | |
---|
157 | /** generate the detail lists |
---|
158 | @param nodes |
---|
159 | */ |
---|
160 | function renderDetail (nodes) { |
---|
161 | renderNodeList (nodes, detail_container_selector); |
---|
162 | } |
---|
163 | |
---|
164 | |
---|
165 | |
---|
166 | /** generate a grouped (by type) list of nodes |
---|
167 | @param nodes - accepts an array of nodes (like in data.nodes) |
---|
168 | */ |
---|
169 | function renderNodeList (nodes, target_container_selector) { |
---|
170 | |
---|
171 | nest = d3.nest() |
---|
172 | .key(function(d) { return d.type; }) |
---|
173 | .sortValues(function(a, b) { return d3.ascending(a.name, b.name); }) |
---|
174 | .entries(nodes); |
---|
175 | |
---|
176 | target_container = d3.select(target_container_selector); |
---|
177 | target_container.selectAll("div.node-detail").remove(); |
---|
178 | |
---|
179 | var group_divs = target_container.selectAll("div.node-detail").data(nest) |
---|
180 | .enter().append("div") |
---|
181 | .attr("id", function (d) { return "detail-" + d.key }) |
---|
182 | .classed("node-detail cmds-ui-block", 1) |
---|
183 | // collapse groups in index, but expand right away in detail view |
---|
184 | .classed("init-show", (target_container_selector != index_container_selector)); |
---|
185 | |
---|
186 | var group_headers = group_divs.append("div").classed("header", 1) |
---|
187 | .text(function (d) { return d.key + " |" + d.values.length + "|"}); |
---|
188 | |
---|
189 | var list = group_divs.append("div").classed("content",1) |
---|
190 | .append("ul"); |
---|
191 | var item_li = list.selectAll(".node-item") |
---|
192 | .data(function(d) { return d.values; }) |
---|
193 | .enter().append("li") |
---|
194 | .attr("class", "node-item"); |
---|
195 | item_li.append("span") |
---|
196 | /* .text(function (d) { return d.name})*/ |
---|
197 | .text(renderItemText) |
---|
198 | .on("click", function(d) { d.selected= d.selected ? 0 : 1 ; updateSelected() }); |
---|
199 | |
---|
200 | |
---|
201 | /* slightly different behaviour for the main-index and rendering of the selected nodes in the detail-view */ |
---|
202 | // console.log("target_container:" + target_container_selector); |
---|
203 | if (target_container_selector == index_container_selector) { |
---|
204 | index_container = target_container; |
---|
205 | item_li.attr("id", function (d) { return "n-" + d.name }) |
---|
206 | .attr("title", function(d) { return d.id }); |
---|
207 | item_li.classed("highlight", function (d) { return d.selected }); |
---|
208 | /* item_li.classed("highlight", liveSelected);*/ |
---|
209 | |
---|
210 | |
---|
211 | } else { |
---|
212 | var item_detail = item_li.append("div"); |
---|
213 | /* .classed("node-detail", 1);*/ |
---|
214 | |
---|
215 | item_detail.append("a") |
---|
216 | .attr("href",function (d) { if (d.type.toLowerCase()=='datcat') return d.id |
---|
217 | else return comp_reg_url + d.id }) |
---|
218 | .text(function (d) { return d.id }); |
---|
219 | |
---|
220 | profile_item_detail = item_detail.filter(function(d, i) { return d.type.toLowerCase()=='profile' }); |
---|
221 | profile_item_detail.append("a") |
---|
222 | .attr("target",'_blank') |
---|
223 | .attr("href",function (d) { return 'profiles/' + d.key + '.html' }) |
---|
224 | .text(' html-view '); |
---|
225 | |
---|
226 | /* profile_item_detail.append("a")*/ |
---|
227 | /* item_detail.append("a") |
---|
228 | .classed("scan", function (d) { return !(d.type=='Profile') } ) |
---|
229 | .attr("target",'_blank') |
---|
230 | .attr("href",function (d) { if (d.type=='Profile') { return mdrepo_url_search + 'cmd.profile=%22' + d.id + '%22'; } |
---|
231 | else { return mdrepo_url_scan + 'cmd:' + d.name; } } ) |
---|
232 | .text(' mdrepo-view ');*/ |
---|
233 | |
---|
234 | item_detail_detail = item_detail.append("div").html( |
---|
235 | function (d) { |
---|
236 | var detail_info_div = getDetailInfo(d.type.toLowerCase(), d.key); |
---|
237 | if (detail_info_div) { |
---|
238 | return detail_info_div |
---|
239 | } else { |
---|
240 | return "<div>No detail</div>"; |
---|
241 | } |
---|
242 | }); |
---|
243 | |
---|
244 | item_detail_detail.classed("node-detail", 1); |
---|
245 | |
---|
246 | } |
---|
247 | |
---|
248 | handleUIBlock($(target_container_selector).find(".node-detail.cmds-ui-block")); |
---|
249 | |
---|
250 | } |
---|
251 | |
---|
252 | function renderItemText(d) { |
---|
253 | if (show_count) { |
---|
254 | return d.name + ' |' + d.count + '|'; |
---|
255 | } |
---|
256 | else { |
---|
257 | return d.name; |
---|
258 | } |
---|
259 | } |
---|
260 | |
---|
261 | function filterIndex (search_string){ |
---|
262 | var filtered_index_nodes = data_all.nodes.filter(function(d, i) { |
---|
263 | // console.log(d.name.indexOf(search_string)); |
---|
264 | return d.name.toLowerCase().indexOf(search_string) > -1; |
---|
265 | }); |
---|
266 | |
---|
267 | renderIndex(filtered_index_nodes, index_container_selector); |
---|
268 | } |
---|
269 | |
---|
270 | |
---|
271 | /** render data (data_show) as graph into target-container (graph_container) */ |
---|
272 | function renderGraph (data, target_container) { |
---|
273 | // setting defaults |
---|
274 | // for now, ignore the params, as they are always the same |
---|
275 | |
---|
276 | //data = typeof data !== 'undefined' ? data : dataToShow(nodes_sel); |
---|
277 | |
---|
278 | if ($(this).is('#input-graph')) { |
---|
279 | console.log("graph-source changed! reinitializing graph"); |
---|
280 | initGraph(data); |
---|
281 | } |
---|
282 | |
---|
283 | data = dataToShow(nodes_sel); |
---|
284 | |
---|
285 | target_container = graph_container ; |
---|
286 | |
---|
287 | if (data == null) { |
---|
288 | $(target_container).text("no data to show"); |
---|
289 | return; |
---|
290 | } else { |
---|
291 | $(target_container).text(""); |
---|
292 | } |
---|
293 | |
---|
294 | // compute the maximum number, but only if it will be needed (i.e. node-size=count) |
---|
295 | if (opt("node-size")=="count") { |
---|
296 | var init_count = []; |
---|
297 | data.nodes.forEach(function(d,i){init_count.push(+d.count);}) |
---|
298 | data.count_max = d3.max(init_count); |
---|
299 | data.node_size_ratio = Math.sqrt(data.count_max) / max_circle; |
---|
300 | } |
---|
301 | |
---|
302 | // information about the displayed data |
---|
303 | notifyUser("show nodes: " + data_show.nodes.length + "; " |
---|
304 | + "show links: " + data_show.links.length + "; " |
---|
305 | + "max count:" + data.count_max + "; " |
---|
306 | + "node_size_ration:" + data.node_size_ratio); |
---|
307 | |
---|
308 | |
---|
309 | |
---|
310 | |
---|
311 | var w = $(target_container).width(), |
---|
312 | h = $(target_container).height(); |
---|
313 | |
---|
314 | var ratio = w / (data_all.init_x_max - data_all.init_x_min); |
---|
315 | var node_size_int = parseInt(opt("node-size")); |
---|
316 | var link_distance = parseInt(opt("link-distance")) |
---|
317 | |
---|
318 | // console.log (w + '-' + h); |
---|
319 | var force = d3.layout.force() |
---|
320 | .nodes(data.nodes) |
---|
321 | .links(data.links) |
---|
322 | .size([w, h]) |
---|
323 | |
---|
324 | .friction(parseInt(opt("friction")) / 100 ) |
---|
325 | .linkDistance(parseInt(opt("link-distance"))) |
---|
326 | .gravity(0.5) |
---|
327 | /* Profiles: |
---|
328 | |
---|
329 | .linkDistance(function(d){return link_distance / (d.weight * d.value) }) |
---|
330 | .linkStrength(function(d){return d.weight})*/ |
---|
331 | //.charge(parseInt(opt("charge")) * -1) |
---|
332 | .charge(function(d) { if (opt("node-size")=="count") |
---|
333 | {var node_charge = (Math.sqrt(d.count)<=min_circle) ? min_circle : Math.sqrt(d.count) / data.node_size_ratio; |
---|
334 | //console.log (node_charge + ':' + d.count); |
---|
335 | return node_charge * -1 * parseInt(opt("charge")); |
---|
336 | } |
---|
337 | //{ return -d.count * parseInt(opt("charge")); } |
---|
338 | else { return parseInt(opt("charge")) * -1} }) |
---|
339 | .on("tick", tick) |
---|
340 | .start(); |
---|
341 | if (opt("layout")=='freeze') { |
---|
342 | data.nodes.forEach(function(d) { d.fixed=true }); |
---|
343 | } else { |
---|
344 | data.nodes.forEach(function(d) { d.fixed=false}); |
---|
345 | } |
---|
346 | |
---|
347 | |
---|
348 | // console.log ("gravity: " + force.gravity() ); |
---|
349 | |
---|
350 | // remove old render: |
---|
351 | d3.select(graph_container_selector).selectAll("svg").remove(); |
---|
352 | |
---|
353 | svg = d3.select(graph_container_selector).append("svg:svg") |
---|
354 | .attr("width", w) .attr("height", h); |
---|
355 | |
---|
356 | // Per-type markers, as they don't inherit styles. |
---|
357 | if (show_arrows) { |
---|
358 | svg.append("svg:defs").selectAll("marker") |
---|
359 | .data(["uses"]) |
---|
360 | .enter().append("svg:marker") |
---|
361 | .attr("id", String) |
---|
362 | .attr("viewBox", "0 -5 10 10") |
---|
363 | .attr("refX", 15) |
---|
364 | .attr("refY", -1.5) |
---|
365 | .attr("markerWidth", 6) |
---|
366 | .attr("markerHeight", 6) |
---|
367 | .attr("orient", "auto") |
---|
368 | .append("svg:path") |
---|
369 | .attr("d", "M0,-3L10,0L0,3"); |
---|
370 | } |
---|
371 | |
---|
372 | var path = svg.append("svg:g").selectAll("path") |
---|
373 | .data(force.links()) |
---|
374 | .enter().append("svg:path") |
---|
375 | /* .attr("class", function(d) { return "link uses"; })*/ |
---|
376 | .classed("link", 1) |
---|
377 | .classed("uses", 1) |
---|
378 | .classed("highlight", function(d) { d.highlight } ) |
---|
379 | .attr("marker-end", function(d) { return "url(#uses)"; }) |
---|
380 | .style("stroke-width", function(d) { return Math.sqrt(d.value); }); |
---|
381 | /* .style("stroke-width", function(d) { return d.value });*/ |
---|
382 | |
---|
383 | path.append("title").text(function(d){ return d.value }); |
---|
384 | |
---|
385 | var gnodes = svg.append("svg:g") |
---|
386 | .selectAll("g.node") |
---|
387 | .data(force.nodes()) |
---|
388 | .enter().append("g") |
---|
389 | .attr("class", function(d) { return "node type-" + d.type.toLowerCase()}) |
---|
390 | .classed("selected", function(d) { return d.selected; }) |
---|
391 | .call(force.drag); |
---|
392 | |
---|
393 | // dragging of all selected nodes on freeze layout |
---|
394 | // this does not work yet |
---|
395 | /*if (opt("layout")=="freeze") { |
---|
396 | gnodes.on("mousedown", function() { |
---|
397 | var m0 = d3.mouse(this); |
---|
398 | |
---|
399 | gnodes.on("mousemove", function() { |
---|
400 | var m1 = d3.mouse(this), |
---|
401 | x0 = Math.min(w, m0[0], m1[0]), |
---|
402 | y0 = Math.min(w, m0[1], m1[1]), |
---|
403 | x1 = Math.max(0, m0[0], m1[0]), |
---|
404 | y1 = Math.max(0, m0[1], m1[1]); |
---|
405 | // console.log("DEBUG: mousedown: " + (x1-x0) + ( y1-y0)); |
---|
406 | x_d = (x1 - x0); |
---|
407 | y_d = (y1 - y0); |
---|
408 | // y_d = d.y - d.py; |
---|
409 | // x_d = d.x - d.px; |
---|
410 | |
---|
411 | nodes_sel.forEach(function (d) { |
---|
412 | d.x += x_d; |
---|
413 | d.y += y_d; |
---|
414 | }); |
---|
415 | |
---|
416 | }); |
---|
417 | |
---|
418 | gnodes.on("mouseup", function() { |
---|
419 | gnodes.on("mousemove", null).on("mouseup", null); |
---|
420 | }); |
---|
421 | |
---|
422 | d3.event.preventDefault(); |
---|
423 | }); |
---|
424 | } |
---|
425 | */ |
---|
426 | gnodes.append("svg:circle") |
---|
427 | /* .attr("r", 6)*/ |
---|
428 | .on("click", function(d) {d.selected= d.selected ? 0 : 1; updateSelected() }) |
---|
429 | .on("mouseover", highlight()).on("mouseout", unhighlight()) |
---|
430 | .attr("r", function(d) { if (opt("node-size")=="count") |
---|
431 | {return (Math.sqrt(d.count)<=min_circle) ? min_circle : Math.sqrt(d.count) / data.node_size_ratio; |
---|
432 | } |
---|
433 | else { return node_size_int; } |
---|
434 | }) |
---|
435 | |
---|
436 | gnodes.append("title") |
---|
437 | /* .text(function (d) { return d.name + ' |' + d.count + '|' })*/ |
---|
438 | .text(renderItemText); |
---|
439 | |
---|
440 | |
---|
441 | |
---|
442 | |
---|
443 | /* |
---|
444 | svg.selectAll("circle") |
---|
445 | .attr("class", function(d) { return "type-" + d.type.toLowerCase()}) |
---|
446 | .classed("selected", function(d) { return d.selected; }) |
---|
447 | .on("click", function(d) {d.selected= d.selected ? 0 : 1; updateSelected() }) |
---|
448 | .on("mouseover", highlight("in")).on("mouseout", highlight("out")); |
---|
449 | */ |
---|
450 | |
---|
451 | // A copy of the text with a thick white stroke for legibility. |
---|
452 | //if (opt("labels") =='show') { |
---|
453 | gnodes.append("svg:text") |
---|
454 | .attr("x", 8) |
---|
455 | .attr("y", ".31em") |
---|
456 | .attr("class", "shadow") |
---|
457 | .classed("hide", opt("labels")=='hide') |
---|
458 | .text(function(d) { return d.name; }); |
---|
459 | gnodes.append("svg:text") |
---|
460 | .attr("x", 8) |
---|
461 | .attr("y", ".31em") |
---|
462 | .classed("hide", function(d) { return !d.selected && opt("labels")=='hide'}) |
---|
463 | .text(function(d) { return d.name; }); |
---|
464 | //} |
---|
465 | |
---|
466 | |
---|
467 | var tick_counter=0; |
---|
468 | function tick(e) { |
---|
469 | var link_distance_int = parseInt(opt("link-distance")); |
---|
470 | var k = 10 * e.alpha; |
---|
471 | if (opt("layout")=='dot') { |
---|
472 | var offset = data_all.init_x_min; |
---|
473 | /*data.links.forEach(function(d, i) { |
---|
474 | d.source.x = (d.source.init_x / 150 * link_distance_int) ; |
---|
475 | d.target.x = (d.target.init_x / 150 * link_distance_int); |
---|
476 | |
---|
477 | });*/ |
---|
478 | |
---|
479 | data.nodes.forEach (function(d,i) { |
---|
480 | d.x = d.init_x * ratio - link_distance_int; |
---|
481 | }); |
---|
482 | |
---|
483 | } else if (opt("layout")=='weak-tree') { |
---|
484 | data.links.forEach(function(d, i) { |
---|
485 | d.source.x -= k; |
---|
486 | d.target.x += k; |
---|
487 | }); |
---|
488 | } else if (opt("layout")=='vertical-tree') { |
---|
489 | var ky= 1.4 * e.alpha, kx = .4 * e.alpha; |
---|
490 | data.links.forEach(function(d, i) { |
---|
491 | if (d.source.level==data_all.level_min) { d.source.y = first_level_margin }; |
---|
492 | // d.target.x += (d.source.x - d.target.x) * kx; |
---|
493 | d.target.y += (d.source.y - d.target.y + link_distance_int) * ky; |
---|
494 | }); |
---|
495 | } else if (opt("layout")=='horizontal-tree') { |
---|
496 | var kx= 1.4 * e.alpha, ky = .4 * e.alpha; |
---|
497 | data.links.forEach(function(d, i) { |
---|
498 | if (d.source.level==data_all.level_min) { d.source.x = first_level_margin }; |
---|
499 | //d.target.y += (d.source.y - d.target.y) * ky; |
---|
500 | d.target.x += (d.source.x - d.target.x + link_distance_int ) * kx; |
---|
501 | }); |
---|
502 | } |
---|
503 | /* parent foci |
---|
504 | var kx = 1.2 * e.alpha; |
---|
505 | data.links.forEach(function(d, i) { |
---|
506 | d.target.x += (d.target.level * link_distance - d.target.x) * kx; |
---|
507 | });*/ |
---|
508 | |
---|
509 | tick_counter ++; |
---|
510 | if (tick_counter % 2 == 0) {transform(); } |
---|
511 | } // end tick() |
---|
512 | |
---|
513 | |
---|
514 | function transform () { |
---|
515 | |
---|
516 | path.attr("d", function(d) { |
---|
517 | // links as elliptical arc path segments |
---|
518 | if (opt("curve")=="arc") |
---|
519 | { var dx = d.target.x - d.source.x, |
---|
520 | dy = d.target.y - d.source.y, |
---|
521 | dr = Math.sqrt(dx * dx + dy * dy); |
---|
522 | return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y; |
---|
523 | } else { |
---|
524 | // or straight |
---|
525 | return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; |
---|
526 | } |
---|
527 | }); |
---|
528 | |
---|
529 | /*circle.attr("cx", function(d) {return d.x;}) |
---|
530 | .attr("cy", function(d) {return d.y;});*/ |
---|
531 | gnodes.attr("transform", function(d) { |
---|
532 | return "translate(" + d.x + "," + d.y + ")"; |
---|
533 | }); |
---|
534 | |
---|
535 | /* textgroup.attr("transform", function(d) { |
---|
536 | return "translate(" + d.x + "," + d.y + ")"; |
---|
537 | });*/ |
---|
538 | } |
---|
539 | |
---|
540 | // Highlight selected nodes using the quadtree. |
---|
541 | svg.on("mousedown", function() { |
---|
542 | var m0 = d3.mouse(this); |
---|
543 | |
---|
544 | var rect = d3.select(this).append("rect") |
---|
545 | .style("fill", "#999") |
---|
546 | .style("fill-opacity", .5); |
---|
547 | |
---|
548 | d3.select(window).on("mousemove", function() { |
---|
549 | var m1 = d3.mouse(rect.node()), |
---|
550 | x0 = Math.min(w, m0[0], m1[0]), |
---|
551 | y0 = Math.min(w, m0[1], m1[1]), |
---|
552 | x1 = Math.max(0, m0[0], m1[0]), |
---|
553 | y1 = Math.max(0, m0[1], m1[1]); |
---|
554 | // console.log("DEBUG: mousedown: " + (x1-x0) + ( y1-y0)); |
---|
555 | selectNodes(data.nodes, x0, y0, x1, y1); |
---|
556 | rect.attr("x", x0).attr("y", y0).attr("width", x1 - x0).attr("height", y1 - y0); |
---|
557 | |
---|
558 | }); |
---|
559 | |
---|
560 | d3.select(window).on("mouseup", function() { |
---|
561 | // only change selection, if the rectangle was big enough |
---|
562 | // (mainly to prevent clearing of the graph on clicks that look like mousemoves to the system) |
---|
563 | if (rect.attr("width") > select_rect_min_size && rect.attr("height") > select_rect_min_size) { |
---|
564 | updateSelected(); |
---|
565 | } |
---|
566 | rect.remove(); |
---|
567 | d3.select(window).on("mousemove", null).on("mouseup", null); |
---|
568 | }); |
---|
569 | |
---|
570 | d3.event.preventDefault(); |
---|
571 | }); |
---|
572 | } // end renderGraph |
---|
573 | |
---|
574 | |
---|
575 | /** loads detail info about individual nodes (in html) from separate file |
---|
576 | later used in renderDetail() |
---|
577 | invoked during the (jquery-)initalization */ |
---|
578 | function loadDetailInfo () { |
---|
579 | |
---|
580 | $(detail_info_holder_selector).load(detail_file,function(data) { |
---|
581 | $(detail_container_selector).find("h3").after( |
---|
582 | '<div id="detail-summary-overall" class="cmds-ui-block init-show" ><div class="header">Overview</div><div class="content">' |
---|
583 | + getDetailInfo("summary", "overall") + '</div></div>'); |
---|
584 | |
---|
585 | handleUIBlock($(detail_container_selector).find(".cmds-ui-block")); |
---|
586 | |
---|
587 | // only render detail for initially selected nodes, after the detail info has been loaded |
---|
588 | if (nodes_sel) { renderDetail(nodes_sel) }; |
---|
589 | |
---|
590 | }); |
---|
591 | |
---|
592 | // loading css to store in extra variable, for later use = injecting into exported SVG |
---|
593 | $.get("scripts/style/smc-graph.css", function(data) { |
---|
594 | // console.log(data) |
---|
595 | css = data |
---|
596 | }); |
---|
597 | |
---|
598 | } |
---|
599 | |
---|
600 | function getDetailInfo(type, id) { |
---|
601 | //notify("getDetailInfo: #" + type + "-" + id ); |
---|
602 | var d = $(detail_info_holder_selector).find("#" + type + "-" + id ); |
---|
603 | // notify(d); |
---|
604 | return d.html(); |
---|
605 | } |
---|
606 | |
---|
607 | /** generates a base64-data encoded url out of the current svg |
---|
608 | does some preprocessing: injects the css and sets the @viewBox, @width and @height attributes to ensure, |
---|
609 | that everything is visible in the exported svg. |
---|
610 | later perhaps even exporting to server, for rendering to PNG, PDF |
---|
611 | http://d3export.cancan.cshl.edu/ |
---|
612 | called on mousedown of the download-link, so assumes the <a>-element as this |
---|
613 | */ |
---|
614 | function genDownload (event) { |
---|
615 | //console.log("genDownload:" + this); |
---|
616 | |
---|
617 | var svg_w = svg.attr("width"); |
---|
618 | var svg_h = svg.attr("height"); |
---|
619 | var bounds = graphBounds(); |
---|
620 | var margin = 30; |
---|
621 | var link_dist = parseInt(opt("link-distance")); |
---|
622 | |
---|
623 | var x, y, w, h; |
---|
624 | x = Math.floor(bounds["x-min"]) - margin |
---|
625 | y = Math.floor(bounds["y-min"]) - margin |
---|
626 | // add extra space to the right, because of the possible labels |
---|
627 | w = (bounds["width"] > svg_w) ? bounds["width"] + 2 * margin + link_dist : svg_w + link_dist; |
---|
628 | h = (bounds["height"] > svg_h) ? bounds["height"] + 2 * margin : svg_h; |
---|
629 | |
---|
630 | var viewBox = x + " " + y + " " + w + " " + h; |
---|
631 | |
---|
632 | svg.attr("title", "SMC Browser - export") |
---|
633 | .attr("version", 1.1) |
---|
634 | .attr("viewBox", viewBox) |
---|
635 | .attr("width", w) |
---|
636 | .attr("height", h) |
---|
637 | .attr("xmlns", "http://www.w3.org/2000/svg"); |
---|
638 | var style = svg.append("style" ); |
---|
639 | style.attr("type", 'text/css'); |
---|
640 | style.text(css); |
---|
641 | |
---|
642 | var html = svg.node().parentNode.innerHTML; |
---|
643 | |
---|
644 | /* $(html).append("<style type='text/css'><![CDATA[" + css + "]]> </style>" );*/ |
---|
645 | |
---|
646 | |
---|
647 | //console.log(html); |
---|
648 | |
---|
649 | $(this).attr("title", "smc-browser-export.svg") |
---|
650 | .attr("target", "_blank") |
---|
651 | .attr("href-lang", "image/svg+xml") |
---|
652 | .attr("href", "data:image/svg+xml;base64,\n" + btoa(html)); |
---|
653 | |
---|
654 | } |
---|
655 | |
---|
656 | |
---|
657 | /** select the nodes within the specified rectangle. */ |
---|
658 | function selectNodes(nodes, x0, y0, x3, y3) { |
---|
659 | |
---|
660 | var points = []; |
---|
661 | nodes.forEach(function(n) { |
---|
662 | if (n && (n.x >= x0) && (n.x < x3) && (n.y >= y0) && (n.y < y3)) { |
---|
663 | points.push(n); |
---|
664 | n.selected = 1; |
---|
665 | } else { |
---|
666 | n.selected = 0; |
---|
667 | } |
---|
668 | /* return x1 >= x3 || y1 >= y3 || x2 < x0 || y2 < y0;*/ |
---|
669 | }); |
---|
670 | return points; |
---|
671 | } |
---|
672 | |
---|
673 | function selectNodeByKey(nodes_keys) { |
---|
674 | |
---|
675 | // get selected nodes (if any) from param |
---|
676 | var selected_match = 0; |
---|
677 | for (var i = 0; i < nodes_keys.length; i++) |
---|
678 | { if (data_all.nodes_index[nodes_keys[i]]) { |
---|
679 | data_all.nodes_index[nodes_keys[i]].selected = 1; |
---|
680 | selected_match ++; |
---|
681 | } |
---|
682 | } |
---|
683 | // if something was selected, update and render Graph and Detail |
---|
684 | if (selected_match) { updateSelected();} |
---|
685 | } |
---|
686 | |
---|
687 | function updateSelected () { |
---|
688 | |
---|
689 | // don't change the selected nodes on freeze-layout |
---|
690 | if (opt("layout")!='freeze') { |
---|
691 | nodes_sel = data_all.nodes.filter(function (d) { return d.selected }); |
---|
692 | |
---|
693 | // update param |
---|
694 | var selected = []; |
---|
695 | nodes_sel.forEach(function(d) { selected.push(d.key) }); |
---|
696 | $("#navigate").data("qi").setParamValue("selected",selected.join()); |
---|
697 | renderDetail(nodes_sel); |
---|
698 | |
---|
699 | } |
---|
700 | |
---|
701 | renderGraph(); |
---|
702 | |
---|
703 | // just need to highlight the selected nodes |
---|
704 | d3.select(index_container_selector).selectAll("li").classed("highlight", function (d) { return d.selected }); |
---|
705 | // renderIndex(); - this would be unnecessary and too expensive |
---|
706 | } |
---|
707 | |
---|
708 | |
---|
709 | /** Returns an event handler for highlighting the path of selected (mouseover) node. |
---|
710 | */ |
---|
711 | function highlight() { |
---|
712 | return function(d, i) { |
---|
713 | // console.log ("fade:" + d.key); |
---|
714 | var connected_subgraph_in = neighboursWithLinks(data_show, d,'in', -1); |
---|
715 | var connected_subgraph_out = neighboursWithLinks(data_show, d,'out', -1); |
---|
716 | var connected_subgraph = {"nodes": [], "links": []}; |
---|
717 | connected_subgraph.nodes = connected_subgraph.nodes.concat(connected_subgraph_in.nodes).concat(connected_subgraph_out.nodes); |
---|
718 | connected_subgraph.links = connected_subgraph.links.concat(connected_subgraph_in.links).concat(connected_subgraph_out.links); |
---|
719 | add_lookups(connected_subgraph); |
---|
720 | svg.selectAll("path.link") |
---|
721 | /* .filter( d.source.index != i && d.target.index != i; })*/ |
---|
722 | /* .transition()*/ |
---|
723 | .classed("highlight", function(p) { return connected_subgraph.links_index[p.source.key + ',' + p.target.key] }) |
---|
724 | .classed("fade", function(p) { return !(connected_subgraph.links_index[p.source.key + ',' + p.target.key]) }); |
---|
725 | |
---|
726 | connected = svg.selectAll("g.node").filter(function(d) { |
---|
727 | return connected_subgraph.nodes_in[d.key] || connected_subgraph.nodes_out[d.key] |
---|
728 | }) |
---|
729 | .classed("highlight", 1) |
---|
730 | .classed("fade", 0); |
---|
731 | |
---|
732 | connected.selectAll("text").classed("hide",0); |
---|
733 | not_connected = svg.selectAll("g.node").filter(function(d) { |
---|
734 | return !(connected_subgraph.nodes_in[d.key] || connected_subgraph.nodes_out[d.key]) |
---|
735 | }) |
---|
736 | .classed("highlight",0) |
---|
737 | .classed("fade",1); |
---|
738 | not_connected.selectAll("text").classed("hide",1); |
---|
739 | |
---|
740 | /*svg.selectAll("circle") |
---|
741 | /\* .filter( d.source.index != i && d.target.index != i; })*\/ |
---|
742 | /\* .transition()*\/ |
---|
743 | .classed("highlight", function(d) { return connected_subgraph.nodes_in[d.key] || connected_subgraph.nodes_out[d.key] }) |
---|
744 | .classed("fade", function(d) { return !(connected_subgraph.nodes_in[d.key] || connected_subgraph.nodes_out[d.key]) });*/ |
---|
745 | /* |
---|
746 | if (opt("labels") =='show') { |
---|
747 | gnodes.append("svg:text") |
---|
748 | .attr("x", 8) |
---|
749 | .attr("y", ".31em") |
---|
750 | .attr("class", "shadow") |
---|
751 | .text(function(d) { return d.name; }); |
---|
752 | gnodes.append("svg:text") |
---|
753 | .attr("x", 8) |
---|
754 | .attr("y", ".31em") |
---|
755 | .text(function(d) { return d.name; }); |
---|
756 | } |
---|
757 | */ |
---|
758 | }; |
---|
759 | } |
---|
760 | |
---|
761 | function unhighlight() { |
---|
762 | return function(d, i) { |
---|
763 | |
---|
764 | svg.selectAll("path.link") |
---|
765 | .classed("highlight", 0 ) |
---|
766 | .classed("fade", 0 ); |
---|
767 | |
---|
768 | var gnodes = svg.selectAll("g.node") |
---|
769 | .classed("highlight", 0) |
---|
770 | .classed("fade", 0); |
---|
771 | |
---|
772 | gnodes.selectAll("text").classed("hide",function(d) { return !d.selected && opt("labels")=='hide'}); |
---|
773 | |
---|
774 | }; |
---|
775 | } |
---|
776 | |
---|
777 | |
---|
778 | /** generates the subset of data to display (based on selected nodes + options) |
---|
779 | fills global variable: data_show ! |
---|
780 | */ |
---|
781 | function dataToShow (nodes) { |
---|
782 | data_show = {}; |
---|
783 | data_show.nodes = nodes; |
---|
784 | var data_show_collect = {nodes:[],links:[]}; |
---|
785 | |
---|
786 | nodes.forEach(function(n) { |
---|
787 | var data_add_in = neighboursWithLinks(data_all, n,'in', opt("depth-before")); |
---|
788 | var data_add_out = neighboursWithLinks(data_all, n,'out', opt("depth-after")); |
---|
789 | data_show_collect.nodes = data_show_collect.nodes.concat(data_add_in.nodes).concat(data_add_out.nodes); |
---|
790 | data_show_collect.links = data_show_collect.links.concat(data_add_in.links).concat(data_add_out.links); |
---|
791 | }); |
---|
792 | |
---|
793 | /* deduplicate nodes and edges */ |
---|
794 | data_show.nodes = unique_nodes(nodes.concat(data_show_collect.nodes)); |
---|
795 | data_show.links = unique_links(data_show_collect.links); |
---|
796 | |
---|
797 | // extend the object, with some lookup hashes on neighbourhood |
---|
798 | add_lookups(data_show); |
---|
799 | |
---|
800 | return data_show; |
---|
801 | } |
---|
802 | |
---|
803 | /** generate lookup hashes for neighbours; |
---|
804 | for faster/simpler neighborhood lookup |
---|
805 | from: http://stackoverflow.com/questions/8739072/highlight-selected-node-its-links-and-its-children-in-a-d3-js-force-directed-g |
---|
806 | */ |
---|
807 | function add_lookups(data) { |
---|
808 | |
---|
809 | var links = data.links; |
---|
810 | var neighbours = {"links_index": {}, "nodes_index": {}, |
---|
811 | "nodes_in": {}, "nodes_out": {}, |
---|
812 | "links_in": {}, "links_out": {}}; |
---|
813 | |
---|
814 | data.nodes.forEach(function(d){ |
---|
815 | neighbours.nodes_index[d.key] = d; |
---|
816 | }); |
---|
817 | |
---|
818 | links.forEach(function(d) { |
---|
819 | src_key = d.source.key; |
---|
820 | trg_key = d.target.key; |
---|
821 | // generate lookup hashes for neighbours; |
---|
822 | neighbours.links_index[src_key + "," + trg_key] = d; |
---|
823 | if (d.source) { |
---|
824 | if (! neighbours.nodes_in[trg_key]) { |
---|
825 | neighbours.nodes_in[trg_key] = [d.source]; |
---|
826 | neighbours.links_in[trg_key] = [d]; |
---|
827 | } else { |
---|
828 | neighbours.nodes_in[trg_key].push(d.source); |
---|
829 | neighbours.links_in[trg_key].push(d); |
---|
830 | } |
---|
831 | } |
---|
832 | if (d.target) { |
---|
833 | if (! neighbours.nodes_out[src_key]) { |
---|
834 | neighbours.nodes_out[src_key] = [d.target]; |
---|
835 | neighbours.links_out[src_key] = [d]; |
---|
836 | } else { |
---|
837 | neighbours.nodes_out[src_key].push(d.target); |
---|
838 | neighbours.links_out[src_key].push(d) ; |
---|
839 | } |
---|
840 | } |
---|
841 | }); |
---|
842 | |
---|
843 | data = $.extend(data, neighbours); |
---|
844 | return data; |
---|
845 | } |
---|
846 | |
---|
847 | |
---|
848 | |
---|
849 | /* item_detail.text(function (d) { return "links_in: " + dataShowCount(d.key, "links_in") + "; links_out: " + dataShowCount(d.key, "links_out") ; |
---|
850 | })*/ |
---|
851 | function dataShowCount(n_key, info_type) { |
---|
852 | |
---|
853 | if (data_show[info_type][n_key]) { |
---|
854 | return data_show[info_type][n_key].length; |
---|
855 | } else { |
---|
856 | return 0 |
---|
857 | } |
---|
858 | } |
---|
859 | |
---|
860 | /** determines the min/max x/y position of the visible nodes |
---|
861 | ! neglects the labels! |
---|
862 | |
---|
863 | @returns array of [x_min, y_min, x_max, y_max, (x_max - x_min), (y_max - y_min)] |
---|
864 | */ |
---|
865 | function graphBounds () { |
---|
866 | |
---|
867 | var x_arr = [], y_arr =[]; |
---|
868 | |
---|
869 | data_show.nodes.forEach(function(d,i){x_arr.push(d.x); y_arr.push(d.y)}) |
---|
870 | |
---|
871 | x_min = d3.min(x_arr); |
---|
872 | x_max = d3.max(x_arr); |
---|
873 | y_min = d3.min(y_arr); |
---|
874 | y_max = d3.max(y_arr); |
---|
875 | |
---|
876 | return {"x-min": x_min, "y-min": y_min, "x-max": x_max, "x-max": y_max, "width": (x_max - x_min), "height":(y_max - y_min)} |
---|
877 | } |
---|
878 | |
---|
879 | /** returns appropriate link |
---|
880 | */ |
---|
881 | function neighboring(a, b) { |
---|
882 | return linkedByIndex[a.index + "," + b.index]; |
---|
883 | } |
---|
884 | |
---|
885 | |
---|
886 | /** access function to retrieve the neighbours from the hashes |
---|
887 | @param data base data to search for links (default: data_all) |
---|
888 | @param dir in|out|any - but "any" branches in unexpected ways (because it goes in and out on every level = it takes all the children of the parent) |
---|
889 | @param depth 0-n - go depth-levels; negative depth := no depth restriction = go to the end of the paths; |
---|
890 | @returns a sub-graph |
---|
891 | */ |
---|
892 | function neighboursWithLinks (data, n, dir, depth) { |
---|
893 | // setting defaults |
---|
894 | depth = typeof depth !== 'undefined' ? depth : 1; |
---|
895 | data = typeof data !== 'undefined' ? data : data_all; |
---|
896 | |
---|
897 | if (depth==0) { return {nodes:[], links:[]};} |
---|
898 | |
---|
899 | var n_in = data.nodes_in[n.key] ? data.nodes_in[n.key] : [] ; |
---|
900 | var n_out = data.nodes_out[n.key] ? data.nodes_out[n.key] : [] ; |
---|
901 | var l_in = data.links_in[n.key] ? data.links_in[n.key] : [] ; |
---|
902 | var l_out = data.links_out[n.key] ? data.links_out[n.key] : [] ; |
---|
903 | |
---|
904 | var result_n = {nodes:[], links:[]}; |
---|
905 | if (dir == 'in' ) { result_n.nodes = n_in; result_n.links = l_in; } |
---|
906 | else if (dir == 'out' ) { result_n.nodes = n_out; result_n.links = l_out; } |
---|
907 | else { result_n.nodes = n_out.concat(n_in); result_n.links = l_out.concat(l_in); } |
---|
908 | var n_nextlevel = {nodes:[], links:[]}; |
---|
909 | if (depth > 0 || depth < 0) { |
---|
910 | result_n.nodes.forEach (function(n) |
---|
911 | { var n_neighbours = neighboursWithLinks(data, n, dir, depth - 1); |
---|
912 | n_nextlevel.nodes = n_nextlevel.nodes.concat(n_neighbours.nodes); |
---|
913 | n_nextlevel.links = n_nextlevel.links.concat(n_neighbours.links); |
---|
914 | }) |
---|
915 | } |
---|
916 | result_n.nodes = result_n.nodes.concat(n_nextlevel.nodes); |
---|
917 | result_n.links = result_n.links.concat(n_nextlevel.links); |
---|
918 | |
---|
919 | return result_n; |
---|
920 | |
---|
921 | } |
---|
922 | |
---|
923 | /** deduplicates based on index-property */ |
---|
924 | function unique_nodes(nodes) |
---|
925 | { |
---|
926 | var hash = {}, result = []; |
---|
927 | for ( var i = 0, l = nodes.length; i < l; ++i ) { |
---|
928 | n_key = nodes[i].key; |
---|
929 | if ( !hash[n_key] ) { //it works with objects! in FF, at least |
---|
930 | hash[ n_key ] = true; |
---|
931 | result.push(nodes[i]); |
---|
932 | } |
---|
933 | } |
---|
934 | return result; |
---|
935 | } |
---|
936 | |
---|
937 | /** deduplicates links (based on source-target-index |
---|
938 | based on: http://stackoverflow.com/questions/1890203/unique-for-arrays-in-javascript |
---|
939 | */ |
---|
940 | function unique_links(links) |
---|
941 | { |
---|
942 | var hash = {}, result = []; |
---|
943 | for ( var i = 0, l = links.length; i < l; ++i ) { |
---|
944 | src_key = links[i].source.key; |
---|
945 | trg_key = links[i].target.key; |
---|
946 | key = src_key + "," + trg_key; |
---|
947 | if ( !hash[key] ) { |
---|
948 | hash[ key] = true; |
---|
949 | result.push(links[i]); |
---|
950 | } |
---|
951 | } |
---|
952 | return result; |
---|
953 | } |
---|