1 /** 2 * creates a new zoom behavior 3 */ 4 var zoom = d3.zoom().on("zoom", handle_transformation); 5 6 /** 7 * creates svg object and associated attributes 8 * applies the zoom behavior to svg 9 */ 10 var svg = d3.select("svg.graph") 11 .call(zoom), 12 width = svg.attr("width"), 13 height = svg.attr("height"), 14 zoom_perc; 15 16 /** 17 * scale functions that return y coordinate/color of node depending on group 18 */ 19 var color = d3.scaleOrdinal() 20 .domain(["Citedby", "Input", "Reference"]) 21 .range(['#01d7c0', '#7fa9d4', '#a15eb2']), 22 y_scale = d3.scaleOrdinal() 23 .domain(["Citedby", "Input", "Reference"]) 24 .range([0, 200, 400]); 25 26 /** 27 * creates node object and (default) radius 28 */ 29 var node, 30 r = 10; 31 32 /** 33 * saves highlighted node for click functionality 34 */ 35 var to_remove; 36 37 /** 38 * creates link object 39 */ 40 var link; 41 42 /** 43 * creates a background with a click functionality 44 */ 45 var rect = svg.append("rect") 46 .attr("x", 0) 47 .attr("y", 0) 48 .attr("height", height) 49 .attr("width", width) 50 .style("fill", 'white') 51 .on('click', click_background); 52 53 /** 54 * creates svg object (legend) with text, circles and arrows 55 */ 56 var svg_legend = d3.select("svg.legendsvg"), 57 legend_position = [65,95,125], 58 arrow_legend_position = [0,25], 59 arrow_group_names = ["Citation","Self-Citation"], 60 group_names = ["Cited-by","Input","Reference"], 61 line_type = d3.scaleOrdinal() 62 .domain(["line","dotted"]) 63 .range([("8,0"),("8,8")]), 64 text_info = '', 65 text_abstract = ''; 66 67 var legend = svg_legend.selectAll(".legend") 68 .data(legend_position) 69 .enter() 70 .append("g") 71 .attr("class","legend") 72 .attr("transform", function(d,i) {return "translate(0," + d + ")"; }); 73 74 legend.append("text") 75 .attr("x", 80) 76 .attr("y", 0) 77 .attr("dy", ".35em") 78 .style("text-anchor", "start") 79 .text(function(d,i) {return group_names[i]}); 80 81 legend.append("circle") 82 .attr("r", r) 83 .attr("cx",30-r) 84 .style("fill", color); 85 86 var legend_arrow = svg_legend.selectAll(".legendarr") 87 .data(arrow_legend_position) 88 .enter() 89 .append("g") 90 .attr("class","legendarr") 91 .attr("transform", function(d) {return "translate(0," + d + ")";}); 92 93 legend_arrow.append("line") 94 .attr("x1", 10) 95 .attr("x2", 50) 96 .attr("y1", 10) 97 .attr("y2", 10) 98 .style("stroke-dasharray",line_type) 99 .style("stroke", '#999') 100 .style("stroke-width", "1px") 101 .style('pointer-events', 'none') 102 .attr('marker-end',update_marker('#999',this)); 103 104 legend_arrow.append("text") 105 .attr("x", 80) 106 .attr("y", 10) 107 .attr("dy", ".35em") 108 .style("text-anchor", "start") 109 .text(function(d,i){return arrow_group_names[i]}); 110 111 /** 112 * creates a new simulation 113 * updates the positions of the links and nodes when the 114 state of the layout has changed (simulation has advanced by a tick) 115 */ 116 var simulation = d3.forceSimulation() 117 .force("link", d3.forceLink().id(function(d) {return d.doi;}).distance(50) 118 .strength(function(d) { 119 if (d.group == "Input") {return 0;} 120 else {return 5;} 121 })) 122 .force("collide", d3.forceCollide(function(d) { 123 if (d.group == "Input") {return 70;} 124 else {return 70;} 125 }).strength(0.5)) 126 .force("charge", d3.forceManyBody().strength(0.001)) 127 .force("center", d3.forceCenter(width/2-20, height/2+20)) 128 .force("yscale", d3.forceY() 129 .strength(function(d) { 130 if (d.group == "Input") {return 300;} 131 else {return 200;} 132 }).y(function(d) {return y_scale(d.group)})) 133 .alpha(0.004) 134 .velocityDecay(0.65) 135 .on("end", zoom_to_graph); 136 137 /** 138 * creates group element 139 */ 140 var g = svg.append("g") 141 .attr("class", "everything") 142 143 /** 144 * loads JSON data and calls the update function 145 */ 146 d3.json("json_text.json").then(success,failure) 147 function success(graph) { 148 update(graph.links, graph.nodes); 149 } 150 function failure(graph) { 151 localStorage.setItem("oldjson","keineJson") 152 } 153 154 /** 155 * checks at a fixed interval whether the contents of the JSON file have changed 156 and reloads the program if necessary 157 */ 158 var intervalId = window.setInterval(check_if_json_changed, 500) 159 function check_if_json_changed() { 160 d3.json("json_text.json").then(function(graph) { 161 newjson_string = JSON.stringify(graph) 162 var newjson = CryptoJS.MD5(newjson_string).toString(); 163 oldjson=localStorage.getItem("oldjson") 164 if(newjson !== oldjson){ 165 localStorage.setItem("oldjson", newjson); 166 window.location.reload() 167 } 168 }) 169 } 170 171 /** 172 * calls update functions for links and nodes 173 * adds the nodes, links and tick functionality to the simulation 174 * @param {object} nodes - nodes 175 * @param {object} links - links 176 */ 177 function update(links, nodes) { 178 update_links(links); 179 update_nodes(nodes); 180 181 simulation.nodes(nodes) 182 .on("tick", handle_tick); 183 simulation.force("link") 184 .links(links); 185 186 link.attr('marker-end', function(d) {return update_marker("#999", d.target);}) 187 .style("stroke-dasharray",function(d) {return self_citation(d.source,d.target)? ("8,8"): ("1,0")}); 188 } 189 190 /** 191 * initializes and shows links (edges) 192 * @param {object} links - links 193 */ 194 function update_links(links) { 195 link = g.append("g") 196 .selectAll(".link") 197 .data(links) 198 .enter() 199 .append("line") 200 .style("stroke-width", "1px") 201 .style("stroke", "#999") 202 .attr("class", "link"); 203 } 204 205 /** 206 * initializes and shows nodes with circles, texts and a click functionality 207 * creates a new drag behavior and applies it to the circles 208 * @param {object} nodes - nodes 209 */ 210 function update_nodes(nodes) { 211 node = g.selectAll(".node") 212 .data(nodes) 213 .enter() 214 .append("g") 215 .attr("class", "node") 216 .call(d3.drag() 217 .on("start", start_drag_node) 218 .on("drag", dragged_node) 219 ); 220 221 node.append("circle") 222 .attr("class", "circle") 223 .attr("r", function(d) {return 1.5*r+d.citations*0.15}) 224 .style("fill", function(d){ return color(d.group)}) 225 .on('click', click_node); 226 227 node.append("text") 228 .attr("class", "text") 229 .style("font-size", "15px") 230 .style('pointer-events', 'auto') 231 .text(function (d) {const first_author = d.author[0].split(" ") 232 return first_author[first_author.length-1];}) 233 .on('click', click_node); 234 } 235 236 /** 237 * creates arrowhead and returns its url 238 * @param {string} color - color of arrowhead 239 * @param {string} target - target node 240 */ 241 function update_marker(color, target) { 242 var radius = 1.5*r+target.citations*0.15; 243 svg.append('defs').append('marker') 244 .attr('id',color.replace("#", "")+radius) 245 .attr('viewBox','-0 -5 10 10') 246 .attr('refX',radius+9.5) 247 .attr('refY',0) 248 .attr('orient','auto') 249 .attr('markerWidth',10) 250 .attr('markerHeight',15) 251 .attr('xoverflow','visible') 252 .append('svg:path') 253 .attr('d', 'M 0,-5 L 10 ,0 L 0,5') 254 .attr('fill', color) 255 .style('stroke','none'); 256 return "url(" + color + radius + ")"; 257 }; 258 259 /** 260 * sets color of circle and its links to black and removes the previous highlights 261 * displays overview info of node in textbox 262 * @param {object} node - node 263 */ 264 function click_node(node) { 265 d3.select(this.parentNode).raise(); 266 fix_nodes(node); 267 if(to_remove){ 268 d3.select(to_remove).selectAll(".circle").style("stroke","none") 269 } 270 to_remove = this.parentNode; 271 d3.select(this.parentNode).selectAll(".circle").style("stroke","black") 272 mark_link(node) 273 textbox_content(node) 274 reset_button_highlight() 275 highlight_button("overview") 276 } 277 278 /** 279 * removes the highlights of the circles and their links 280 */ 281 function click_background() { 282 fix_nodes(node); 283 d3.selectAll(".circle").style("stroke", "none") 284 d3.selectAll(".link") 285 .style("stroke", "#999") 286 .attr('marker-end', function(d) {return update_marker('#999', d.target);}) 287 text_abstract = ''; 288 text_info = ''; 289 reset_button_highlight() 290 document.getElementById('textbox').innerHTML = "Click node"; 291 } 292 293 /** 294 * returns true if journals have a common author (self-citation) 295 * @param {object} source - node 296 * @param {object} target - node 297 */ 298 function self_citation(source,target) { 299 return source.author.some(item=>target.author.includes(item)) 300 } 301 302 /** 303 * sets color of link (line and arrowhead) to black if it is directly connected to node 304 * and to grey otherwise 305 * @param {object} node - node 306 */ 307 function mark_link(node) { 308 d3.selectAll(".link") 309 .style("stroke", function(o) { 310 return is_link_for_node(node, o) ? "black" : "#DEDEDE";}) 311 .attr('marker-end', function(o) { 312 return is_link_for_node(node, o) ? update_marker('#000000', o.target) : update_marker('#DEDEDE', o.target);}) 313 } 314 315 /** 316 * returns true if link is directly connected to node and false if it is not 317 * @param {object} node - node 318 * @param {object} link - link 319 */ 320 function is_link_for_node(node, link) { 321 return link.source.index == node.index || link.target.index == node.index; 322 } 323 324 /** 325 * saves text for overview and abstract of node 326 * outputs node info to textbox 327 * @param {object} node - node 328 */ 329 function textbox_content(node) { 330 authors = node.author[0] 331 for (i = 1; i < node.author.length; i++) { 332 authors += (", "+node.author[i]) 333 } 334 text_info = "Title:" + '</br>' + node.name + 335 '</br>' +'</br>'+"Author:"+ '</br>' +authors+'</br>'+'</br>'+"Date:"+'</br>' 336 +node.year+'</br>'+'</br>'+"Journal:"+'</br>'+node.journal+'</br>'+'</br>'+"DOI:" 337 +'</br>'+node.doi+'</br>'+'</br>'+"Citations:" 338 +'</br>'+node.citations; 339 text_abstract = node.abstract; 340 document.getElementById('textbox').innerHTML = text_info; 341 } 342 343 /** 344 * sets color of btn to dark gray 345 * @param {object} btn - button 346 */ 347 function highlight_button(btn) { 348 reset_button_highlight(); 349 document.getElementById(btn).style.background = "#CACACA"; 350 } 351 352 /** 353 * sets color of all buttons to default light gray 354 */ 355 function reset_button_highlight() { 356 document.getElementById("overview").style.background = ''; 357 document.getElementById("abstract").style.background = ''; 358 } 359 360 /** 361 * displays abstract in textbox if a is true, overview text otherwise 362 * @param {bool} a- bool 363 */ 364 function display_abstract(a) { 365 if (text_abstract == '' && text_info == '') { 366 document.getElementById('textbox').innerHTML = "Click node"; 367 } 368 else { 369 if (a == true) { 370 document.getElementById('textbox').innerHTML = text_abstract; 371 } 372 else { 373 document.getElementById('textbox').innerHTML = text_info; 374 } 375 } 376 } 377 378 /** 379 * updates the positions of the links and nodes 380 */ 381 function handle_tick() { 382 link.attr("x1", function (d) {return d.source.x;}) 383 .attr("y1", function (d) {return d.source.y;}) 384 .attr("x2", function (d) {return d.target.x;}) 385 .attr("y2", function (d) {return d.target.y;}); 386 node.attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";}); 387 } 388 389 /** 390 * initializes dragging of the node 391 * @param {object} node - node 392 */ 393 function start_drag_node(node) { 394 d3.select(this).raise(); 395 if (!d3.event.active) 396 simulation.alphaTarget(0.3).restart() 397 node.fx = node.x; 398 node.fy = node.y; 399 fix_nodes(node); 400 } 401 402 /** 403 * applies dragging to the node 404 * @param {object} node - node 405 */ 406 function dragged_node(node) { 407 node.fx = d3.event.x; 408 node.fy = d3.event.y; 409 fix_nodes(node); 410 } 411 412 /** 413 * fix positions of all nodes except for the current node 414 * @param {object} this_node - node 415 */ 416 function fix_nodes(this_node) { 417 node.each(function(d) { 418 if (this_node != d) { 419 d.fx = d.x; 420 d.fy = d.y; 421 } 422 }); 423 } 424 425 /** 426 * applies the transformation (zooming or dragging) to the g element 427 */ 428 function handle_transformation() { 429 d3.select('g').attr("transform", d3.event.transform); 430 } 431 432 /** 433 * transforms svg so that the zoom is adapted to the size of the graph 434 */ 435 function zoom_to_graph() { 436 node_bounds = d3.selectAll("svg.graph").node().getBBox(); 437 svg_bounds = d3.select("rect").node().getBBox(); 438 439 perc_x = width/(node_bounds.width+100); 440 perc_y = height/(node_bounds.height+100); 441 zoom_perc = d3.min([perc_x, perc_y]) 442 443 d3.select('svg') 444 .call(zoom.scaleBy, zoom_perc); 445 } 446 447 /** 448 * transforms svg so that the zoom and drag is reset 449 */ 450 function reset_view() { 451 d3.select('svg') 452 .call(zoom.scaleTo, 1) 453 d3.select('svg') 454 .call(zoom.translateTo, 0.5 * width, 0.5 * height); 455 d3.select('svg') 456 .call(zoom.scaleBy, zoom_perc); 457 } 458 459 /** 460 * save svg as png 461 */ 462 function save_svg(){ 463 var svgString = get_svg_string(svg.node()); 464 svg_string_to_image(svgString, 2*width, 2*height, 'png', save); // passes Blob and filesize String to the callback 465 466 function save( dataBlob, filesize ){ 467 saveAs(dataBlob, 'D3 vis exported to PNG.png'); // FileSaver.js function 468 } 469 }; 470 471 /** 472 * generate svgString 473 * @param {object} svgNode - node 474 */ 475 function get_svg_string(svgNode) { 476 svgNode.setAttribute('xlink', 'http://www.w3.org/1999/xlink'); 477 var cssStyleText = get_css_styles(svgNode); 478 append_css(cssStyleText, svgNode); 479 480 var serializer = new XMLSerializer(); 481 var svgString = serializer.serializeToString(svgNode); 482 svgString = svgString.replace(/(\w+)?:?xlink=/g, 'xmlns:xlink='); // Fix root xlink without namespace 483 svgString = svgString.replace(/NS\d+:href/g, 'xlink:href'); // Safari NS namespace fix 484 485 return svgString; 486 487 function get_css_styles(parentElement) { 488 var selectorTextArr = []; 489 490 // Add Parent element Id and Classes to the list 491 selectorTextArr.push('#' + parentElement.id); 492 for (var c = 0; c < parentElement.classList.length; c++) 493 if (!contains('.'+parentElement.classList[c], selectorTextArr)) 494 selectorTextArr.push('.'+parentElement.classList[c]); 495 496 // Add Children element Ids and Classes to the list 497 var nodes = parentElement.getElementsByTagName("*"); 498 for (var i = 0; i < nodes.length; i++) { 499 var id = nodes[i].id; 500 if (!contains('#'+id, selectorTextArr)) 501 selectorTextArr.push('#' + id); 502 503 var classes = nodes[i].classList; 504 for (var c = 0; c < classes.length; c++) 505 if (!contains('.'+classes[c], selectorTextArr)) 506 selectorTextArr.push('.'+classes[c]); 507 } 508 509 // Extract CSS Rules 510 var extractedCSSText = ""; 511 for (var i = 0; i < document.styleSheets.length; i++) { 512 var s = document.styleSheets[i]; 513 514 try { 515 if(!s.cssRules) continue; 516 } 517 catch(e) { 518 if(e.name !== 'SecurityError') throw e; // for Firefox 519 continue; 520 } 521 522 var cssRules = s.cssRules; 523 for (var r = 0; r < cssRules.length; r++) { 524 if (contains(cssRules[r].selectorText, selectorTextArr)) 525 extractedCSSText += cssRules[r].cssText; 526 } 527 } 528 return extractedCSSText; 529 530 function contains(str,arr) { 531 return arr.indexOf(str) === -1 ? false : true; 532 } 533 } 534 535 function append_css(cssText, element) { 536 var styleElement = document.createElement("style"); 537 styleElement.setAttribute("type","text/css"); 538 styleElement.innerHTML = cssText; 539 var refNode = element.hasChildNodes() ? element.children[0] : null; 540 element.insertBefore(styleElement, refNode); 541 } 542 } 543 544 /** 545 * convert svgString to image and export it 546 * @param {object} svgString - svgString 547 * @param {object} width - width of image 548 * @param {object} height - height of image 549 * @param {object} format - format to save image in 550 * @param {object} callback - callback function 551 */ 552 function svg_string_to_image( svgString, width, height, format, callback ) { 553 var format = format ? format : 'png'; 554 555 var imgsrc = 'data:image/svg+xml;base64,'+ btoa(unescape(encodeURIComponent(svgString))); // Convert SVG string to data URL 556 557 var canvas = document.createElement("canvas"); 558 var context = canvas.getContext("2d"); 559 560 canvas.width = width; 561 canvas.height = height; 562 563 var image = new Image(); 564 image.onload = function() { 565 context.clearRect(0, 0, width, height); 566 context.drawImage(image, 0, 0, width, height); 567 568 canvas.toBlob(function(blob) { 569 var filesize = Math.round(blob.length/1024) + ' KB'; 570 if (callback) callback(blob, filesize); 571 }); 572 }; 573 image.src = imgsrc; 574 }