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 }