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 }