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