/** * creates a new zoom behavior */ var zoom = d3.zoom().on("zoom", handle_zoom); /** * creates svg object and associated attributes * applies the zoom behavior to svg */ var svg = d3.select("svg.graph") .call(zoom), width = svg.attr("width"), height = svg.attr("height"), perc; /** * scale functions that return y coordinate/color of node depending on group */ var color = d3.scaleOrdinal() .domain(["height", "input", "depth"]) .range([' #01d7c0', ' #8b90fe ', ' #a15eb2 ']), y_scale = d3.scaleOrdinal() .domain(["height", "input", "depth"]) .range([0, 200, 400]), to_remove; /** * creates node object and (default) radius */ var node, r = 10; /** * creates link object */ var link; /** * creates a background with a click functionality */ var rect = svg.append("rect") .attr("x", 0) .attr("y", 0) .attr("height", height) .attr("width", width) .style("fill", 'white') .on('click', click_rect); /** * creates svg object (legend) and associated attributes */ var svg_legend = d3.select("svg.legendsvg"), legend_position = [65,95,125], arrow_legend_position = [0,25], arrow_group_names = ["citation","self-citation"], group_names = ["cited by","input","reference"], line_type = d3.scaleOrdinal() .domain(["line","dotted"]) .range([("8,0"),("8,8")]), text_info = '', text_abstract = ''; var legend = svg_legend.selectAll(".legend") .data(legend_position) .enter() .append("g") .attr("class","legend") .attr("transform", function(d,i) {return "translate(0," + d + ")"; }); legend.append("text") .attr("x", 80) .attr("y", 0) .attr("dy", ".35em") .style("text-anchor", "start") .text(function(d,i) {return group_names[i]}); legend.append("circle") .attr("r", r) .attr("cx",30-r) .style("fill", color); var legend_arrow = svg_legend.selectAll(".legendarr") .data(arrow_legend_position) .enter() .append("g") .attr("class","legendarr") .attr("transform", function(d) { return "translate(0," + d + ")"; }); legend_arrow.append("line") .attr("x1", 10) .attr("x2", 50) .attr("y1", 10) .attr("y2", 10) .style("stroke-dasharray",line_type) .style("stroke", '#999') .style("stroke-width", "1px") .style('pointer-events', 'none') .attr('marker-end',update_marker('#999',this)); legend_arrow.append("text") .attr("x", 80) .attr("y", 10) .attr("dy", ".35em") .style("text-anchor", "start") .text(function(d,i){return arrow_group_names[i]}); /** * creates a new simulation * updates the positions of the links and nodes when the state of the layout has changed (simulation has advanced by a tick) */ var simulation = d3.forceSimulation() .force("link", d3.forceLink().id(function(d) {return d.doi;}).distance(50).strength(function(d) { if (d.group == "input") {return 0;} else {return 5;} })) .force("collide", d3.forceCollide(function(d) { if (d.group == "input") {return 100;} else {return 65;} }).strength(0.5)) .force("charge", d3.forceManyBody().strength(0.001)) .force("center", d3.forceCenter(width/2, height/2+20)) .force("yscale", d3.forceY().strength(function(d) { if (d.group == "input") {return 1000;} else {return 50;} }).y(function(d) {return y_scale(d.group)})) .alpha(0.005) .on("end", zoom_to); /** * creates group element */ var g = svg.append("g") .attr("class", "everything") /** * loads JSON data and calls the update function */ d3.json("json_text.json").then(function(graph) { update(graph.links, graph.nodes); }) /** * calls update functions for links and nodes * adds the nodes, links and tick functionality to the simulation * @param {object} nodes - nodes * @param {object} links - links */ function update(links, nodes) { update_links(links); update_nodes(nodes); simulation .nodes(nodes) .on("tick", handle_tick); simulation.force("link") .links(links); link.attr('marker-end', function(d) {return update_marker("#999", d.target);}) .style("stroke-dasharray",function(d){return self_citation(d.source,d.target)? ("8,8"): ("1,0")}); } /** * initializes and shows links * @param {object} links - links */ function update_links(links) { link = g.append("g") .selectAll(".link") .data(links) .enter() .append("line") .style("stroke-width", "1px") .style("stroke", "#999") .attr("class", "link"); } /** * initializes and shows nodes with circles, texts and a click functionality * creates a new drag behavior and applies it to the circles * @param {object} nodes - nodes */ function update_nodes(nodes) { node = g.selectAll(".node") .data(nodes) .enter() .append("g") .attr("class", "node") .call(d3.drag() .on("start", start_drag) .on("drag", dragged) ); node.append("circle") .attr("class", "circle") .attr("r", function(d) {return 1.5*r+d.citations*0.05}) .style("fill", function(d){ return color(d.group)}) .on('click', click_node); node.append("text") .attr("class", "text") .style("font-size", "15px") .style('pointer-events', 'auto') .text(function (d) {const first_author=d.author[0].split(" ") return first_author[first_author.length-1];}) .on('click', click_node); } /** * creates arrowhead and returns its url * @param {string} color - color of arrowhead * @param {string} target - target node */ function update_marker(color, target) { var radius = 1.5*r+target.citations*0.05; svg.append('defs').append('marker') .attr('id',color.replace("#", "")+radius) .attr('viewBox','-0 -5 10 10') .attr('refX',radius+9.5) .attr('refY',0) .attr('orient','auto') .attr('markerWidth',10) .attr('markerHeight',15) .attr('xoverflow','visible') .append('svg:path') .attr('d', 'M 0,-5 L 10 ,0 L 0,5') .attr('fill', color) .style('stroke','none'); return "url(" + color + radius + ")"; }; /** * sets color of circle and its links to black and removes the previous highlights * displays overview info of node in textbox * @param {object} node - node */ function click_node(node) { d3.select(this.parentNode).raise(); fix_nodes(node); if(to_remove){ d3.select(to_remove).selectAll(".circle").style("stroke","none") } to_remove = this.parentNode; d3.select(this.parentNode).selectAll(".circle").style("stroke","black") mark_link(node) textbox_content(node) reset_button_highlight() highlight_button("overview") } /** * removes the highlights of the circles and their links */ function click_rect() { fix_nodes(node); d3.selectAll(".circle").style("stroke", "none") d3.selectAll(".link") .style("stroke", "#999") .attr('marker-end', function(d) {return update_marker('#999', d.target);}) text_abstract=''; text_info=''; reset_button_highlight() document.getElementById('textbox').innerHTML = "Click node"; } /** * returns true if journals have a common author (self-citation) * @param {object} source - node * @param {object} target - node */ function self_citation(source,target) { return source.author.some(item=>target.author.includes(item)) } /** * sets color of link (line and arrowhead) to black if it is directly connected to node * and to grey otherwise * @param {object} node - node */ function mark_link(node) { d3.selectAll(".link") .style("stroke", function(o) { return is_link_for_node(node, o) ? "black" : "#999";}) .attr('marker-end', function(o) { return is_link_for_node(node, o) ? update_marker('#000000', o.target) : update_marker('#999', o.target);}) } /** * returns true if link is directly connected to node and false if it is not * @param {object} node - node * @param {object} link - link */ function is_link_for_node(node, link) { return link.source.index == node.index || link.target.index == node.index; } /** * saves text for overview and abstract of node * outputs node info to textbox * @param {object} node - node */ function textbox_content(node) { text_info = "Title:" + '</br>' + node.name + '</br>' +'</br>'+"Author:"+ '</br>' +node.author+'</br>'+'</br>'+"Date:"+'</br>' +node.year+'</br>'+'</br>'+"Journal:"+'</br>'+node.journal+'</br>'+'</br>'+"doi:" +'</br>'+'<a href="'+node.doi+ '">'+node.doi+'</a>'+'</br>'+'</br>'+"Citations:" +'</br>'+node.citations; text_abstract = node.abstract; document.getElementById('textbox').innerHTML = text_info; } /** * sets color of btn to dark gray * @param {object} btn - button */ function highlight_button(btn) { reset_button_highlight(); document.getElementById(btn).style.background = "#CACACA"; } /** * sets color of all buttons to default light gray */ function reset_button_highlight() { document.getElementById("overview").style.background = ''; document.getElementById("abstract").style.background = ''; } /** * displays abstract in textbox if a is true, overview text otherwise * @param {bool} a- bool */ function display_abstract(a) { if (text_abstract == '' && text_info == '') { document.getElementById('textbox').innerHTML="Click node"; } else { if (a == true) { document.getElementById('textbox').innerHTML = text_abstract; } else { document.getElementById('textbox').innerHTML = text_info; } } } /** * updates the positions of the links and nodes */ function handle_tick() { link.attr("x1", function (d) {return d.source.x;}) .attr("y1", function (d) {return d.source.y;}) .attr("x2", function (d) {return d.target.x;}) .attr("y2", function (d) {return d.target.y;}); node.attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";}); } /** * initializes the dragging of the node * @param {object} node - node */ function start_drag(node) { d3.select(this).raise(); if (!d3.event.active) simulation.alphaTarget(0.3).restart() node.fx = node.x; node.fy = node.y; fix_nodes(node); } /** * applies the dragging to the node * @param {object} node - node */ function dragged(node) { node.fx = d3.event.x; node.fy = d3.event.y; fix_nodes(node); } /** * fix positions of all nodes except for the current node * @param {object} this_node - node */ function fix_nodes(this_node) { node.each(function(d) { if (this_node != d) { d.fx = d.x; d.fy = d.y; } }); } /** * applies the transformation (zooming or dragging) to the g element */ function handle_zoom() { d3.select('g').attr("transform", d3.event.transform); } /** * transforms svg so that the zoom is adapted to the size of the graph */ function zoom_to() { node_bounds = d3.selectAll("svg.graph").node().getBBox(); svg_bounds = d3.select("rect").node().getBBox(); perc_x = width/(node_bounds.width+100); perc_y = height/(node_bounds.height+100); perc = d3.min([perc_x, perc_y]) d3.select('svg') .call(zoom.scaleBy, perc); } /** * transforms svg so that the zoom and drag is reset */ function reset_view() { d3.select('svg') .call(zoom.scaleTo, 1) d3.select('svg') .call(zoom.translateTo, 0.5 * width, 0.5 * height); d3.select('svg') .call(zoom.scaleBy, perc); } /** * save svg as png */ function save_svg(){ var svgString = get_svg_string(svg.node()); svg_string_to_image(svgString, 2*width, 2*height, 'png', save); // passes Blob and filesize String to the callback function save( dataBlob, filesize ){ saveAs(dataBlob, 'D3 vis exported to PNG.png'); // FileSaver.js function } }; /** * generate svgString * @param {object} svgNode - node */ function get_svg_string(svgNode) { svgNode.setAttribute('xlink', 'http://www.w3.org/1999/xlink'); var cssStyleText = get_css_styles(svgNode); append_css(cssStyleText, svgNode); var serializer = new XMLSerializer(); var svgString = serializer.serializeToString(svgNode); svgString = svgString.replace(/(\w+)?:?xlink=/g, 'xmlns:xlink='); // Fix root xlink without namespace svgString = svgString.replace(/NS\d+:href/g, 'xlink:href'); // Safari NS namespace fix return svgString; function get_css_styles(parentElement) { var selectorTextArr = []; // Add Parent element Id and Classes to the list selectorTextArr.push('#' + parentElement.id); for (var c = 0; c < parentElement.classList.length; c++) if (!contains('.'+parentElement.classList[c], selectorTextArr)) selectorTextArr.push('.'+parentElement.classList[c]); // Add Children element Ids and Classes to the list var nodes = parentElement.getElementsByTagName("*"); for (var i = 0; i < nodes.length; i++) { var id = nodes[i].id; if (!contains('#'+id, selectorTextArr)) selectorTextArr.push('#' + id); var classes = nodes[i].classList; for (var c = 0; c < classes.length; c++) if (!contains('.'+classes[c], selectorTextArr)) selectorTextArr.push('.'+classes[c]); } // Extract CSS Rules var extractedCSSText = ""; for (var i = 0; i < document.styleSheets.length; i++) { var s = document.styleSheets[i]; try { if(!s.cssRules) continue; } catch(e) { if(e.name !== 'SecurityError') throw e; // for Firefox continue; } var cssRules = s.cssRules; for (var r = 0; r < cssRules.length; r++) { if (contains(cssRules[r].selectorText, selectorTextArr)) extractedCSSText += cssRules[r].cssText; } } return extractedCSSText; function contains(str,arr) { return arr.indexOf(str) === -1 ? false : true; } } function append_css(cssText, element) { var styleElement = document.createElement("style"); styleElement.setAttribute("type","text/css"); styleElement.innerHTML = cssText; var refNode = element.hasChildNodes() ? element.children[0] : null; element.insertBefore(styleElement, refNode); } } /** * convert svgString to image and export it * @param {object} svgString - svgString * @param {object} width - width of image * @param {object} height - height of image * @param {object} format - format to save image in * @param {object} callback - callback function */ function svg_string_to_image( svgString, width, height, format, callback ) { var format = format ? format : 'png'; var imgsrc = 'data:image/svg+xml;base64,'+ btoa(unescape(encodeURIComponent(svgString))); // Convert SVG string to data URL var canvas = document.createElement("canvas"); var context = canvas.getContext("2d"); canvas.width = width; canvas.height = height; var image = new Image(); image.onload = function() { context.clearRect(0, 0, width, height); context.drawImage(image, 0, 0, width, height); canvas.toBlob(function(blob) { var filesize = Math.round(blob.length/1024) + ' KB'; if (callback) callback(blob, filesize); }); }; image.src = imgsrc; }