diff --git a/index.js b/index.js index 42723c8..05c11fd 100644 --- a/index.js +++ b/index.js @@ -68,7 +68,6 @@ views.map(function(route) { (function(route) { app.use(path.join('/', route, ':corpus'), function(req, res) { - console.log(corpora, req.params.corpus); let corpus = corpora[req.params.corpus], html = "Corpus Not Found"; if (corpus) { diff --git a/public/css/force-directed-graph.css b/public/css/force-directed-graph.css index dc56282..71e9267 100644 --- a/public/css/force-directed-graph.css +++ b/public/css/force-directed-graph.css @@ -17,3 +17,17 @@ fill: yellow; } +.links { + stroke: #546e7a; + stroke-opacity: 0.6; +} + +.nodes { + stroke: #fff; + stroke-width: 1.5px; +} + +.nodes title { + fill: yellow; +} + diff --git a/public/js/force-atlas2-graph.js b/public/js/force-atlas2-graph.js index 5045d28..286e4c6 100644 --- a/public/js/force-atlas2-graph.js +++ b/public/js/force-atlas2-graph.js @@ -30,6 +30,27 @@ // Call to function with anonymous callback loadJSON(url, function(response) { var graph = JSON.parse(response); + + var edges = [], + nodes = []; + for (var i = 0; i < graph.nodes.length; i++) { + nodes.push(i); + } + for (var i = 0; i < graph.links.length; i++) { + var edge = graph.links[i]; + edge.weight = edge.value; + edges.push(edge); + } + + // Create the "community" + var community = jLouvain().nodes(nodes).edges(edges), + res = community(); + + // Affect community for each node + for (var key in res) { + graph.nodes[nodes[key]].group = res[key]; + } + var deleted_links = {}, selectedLinks = graph.links.filter(function(d) { var res = (d.value >= 3); @@ -45,25 +66,6 @@ return res; }); - var edges = [], - nodes = []; - for (var i = 0; i < selectedNodes.length; i++) { - nodes.push(i); - } - for (var i = 0; i < selectedLinks.length; i++) { - var edge = selectedLinks[i]; - edge.weight = edge.value; - edges.push(edge); - } - - // Create the "community" - var community = jLouvain().nodes(nodes).edges(edges), - res = community(); - // Affect community for each node - for (var key in res) { - selectedNodes[nodes[key]].group = res[key]; - } - var i, s, o, @@ -132,6 +134,6 @@ } else { clearTimeout(timeout); }; - }, 10000); + }, 240000); }); } \ No newline at end of file diff --git a/public/js/force-directed-graph-svg.js b/public/js/force-directed-graph-svg.js new file mode 100644 index 0000000..e7aa50b --- /dev/null +++ b/public/js/force-directed-graph-svg.js @@ -0,0 +1,201 @@ +"use strict"; +(function() { + var lastTime = 0; + var vendors = ['webkit', 'moz']; + for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame']; + window.cancelAnimationFrame = + window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { callback(currTime + timeToCall); }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; +}()); + +function initGraph(url, corpus) { + var loc = window.location.pathname, + dir = loc.substring(0, loc.lastIndexOf('/')), + corpus = JSON.parse(corpus); + + var svg = d3.select("svg"), + sketch = svg.append("g"), + container = d3.select("#container"), + color = d3.scaleOrdinal(d3.schemeCategory20), + width = +container.attr("width"), + height = +container.attr("height"); + + var zoom = d3.zoom() + .scaleExtent([-100, 100]) + .on('zoom', zoomFn); + + function zoomFn() { + sketch.attr('transform', 'translate(' + d3.event.transform.x + ',' + d3.event.transform.y + ') scale(' + d3.event.transform.k + ')'); + } + svg.call(zoom); + + var simulation = d3.forceSimulation() + .force("link", d3.forceLink().id(function(d) { + return d.id; + })) + .force("charge", d3.forceManyBody()) + .force("center", d3.forceCenter(width / 2, height / 2)); + + d3.json(url, function(error, graph) { + if (error) throw error; + + var edges = [], + nodes = []; + for (var i = 0; i < graph.nodes.length; i++) { + nodes.push(i); + } + for (var i = 0; i < graph.links.length; i++) { + var edge = graph.links[i]; + edge.weight = edge.value; + edges.push(edge); + } + + // Create the "community" + var community = jLouvain().nodes(nodes).edges(edges), + res = community(); + + // Affect community for each node + for (var key in res) { + graph.nodes[nodes[key]].group = res[key]; + } + + var deleted_links = {}, + selectedLinks = graph.links.filter(function(d) { + var res = (d.value >= 5); + if (!res) { + deleted_links[d.source] = deleted_links[d.source] + 1 || 1; + deleted_links[d.target] = deleted_links[d.target] + 1 || 1; + } + return res; + }), + selectedNodes = graph.nodes.filter(function(d) { + d.value = d.value - (deleted_links[d.id] || 0); + var res = (d.value > 0); + return res; + }); + + var link = sketch + // .append("g") + // .attr("class", "links") + .selectAll("line") + .data(selectedLinks) + .enter().append("line") + .attr("class", "links") + .attr("stroke-width", function(d) { + return 3; + // return d.value; + }); + + link.append("title") + .text(function(d) { + return d.value; + }); + + var node = sketch + // .append("g") + // .attr("class", "nodes") + .selectAll("circle") + .data(selectedNodes) + .enter() + .append("circle") + .attr("class", "nodes") + .attr("r", function(d) { + return 7; + // return 10 * d.specificity; + }) + .attr("fill", function(d) { + return color(d.group); + }) + // .call(d3.drag() + // .on("start", dragstarted) + // .on("drag", dragged) + // .on("end", dragended)) + .on("click", function(d) { + window.open('/' + corpus.in + '/' + d.data.id + '.txt'); + }) + .on("mouseover", function(d) { + d3.select("#subtitle") + .style("opacity", 1) + .text(() => { + return d3.select(this).selectAll("title").text(); + }); + d3.select(this) + .attr("r", 14); + }) + .on("mouseleave", function(d) { + d3.select("#subtitle") + .style("opacity", 0); + d3.select(this) + .attr("r", 7); + }); + + node.append("title") + .text(function(d) { + return d.data.id; + }); + + simulation + .nodes(selectedNodes) + .on("tick", ticked); + + simulation.force("link") + .links(selectedLinks); + + function ticked() { + 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("cx", function(d) { + return d.x; + }) + .attr("cy", function(d) { + return d.y; + }); + } + }); + + function dragstarted(d) { + if (!d3.event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged(d) { + d.fx = d3.event.x; + d.fy = d3.event.y; + } + + function dragended(d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } +} \ No newline at end of file diff --git a/public/js/force-directed-graph.js b/public/js/force-directed-graph.js index be027df..d127190 100644 --- a/public/js/force-directed-graph.js +++ b/public/js/force-directed-graph.js @@ -1,31 +1,53 @@ "use strict"; function initGraph(url, corpus) { + // VAR var loc = window.location.pathname, dir = loc.substring(0, loc.lastIndexOf('/')), corpus = JSON.parse(corpus); - var svg = d3.select("svg"), - container = d3.select("#container"), - color = d3.scaleOrdinal(d3.schemeCategory20), - width = +container.attr("width"), - height = +container.attr("height"); - - var simulation = d3.forceSimulation() - .force("link", d3.forceLink().id(function(d) { - return d.id; - })) - .force("charge", d3.forceManyBody()) - .force("center", d3.forceCenter(width / 2, height / 2)); - - console.log(url); - d3.json(url, function(error, graph) { if (error) throw error; + var canvas = document.querySelector("#container"), + d3Canvas = d3.select("#container"), + color = d3.scaleOrdinal(d3.schemeCategory20), + width = +d3Canvas.attr("width"), + height = +d3Canvas.attr("height"), + radius = 2.5, + context = canvas.getContext("2d"); + + var simulation = d3.forceSimulation() + .force("link", d3.forceLink().id(function(d) { + return d.id; + })) + .force("charge", d3.forceManyBody()) + .force("center", d3.forceCenter(width / 2, height / 2)); + + // FILTER & COMMUNITY + var edges = [], + nodes = []; + for (var i = 0; i < graph.nodes.length; i++) { + nodes.push(i); + } + for (var i = 0; i < graph.links.length; i++) { + var edge = graph.links[i]; + edge.weight = edge.value; + edges.push(edge); + } + + // Create the "community" + var community = jLouvain().nodes(nodes).edges(edges), + res = community(); + + // Affect community for each node + for (var key in res) { + graph.nodes[nodes[key]].group = res[key]; + } + var deleted_links = {}, selectedLinks = graph.links.filter(function(d) { - var res = (d.value >= 3); + var res = (d.value >= 10); if (!res) { deleted_links[d.source] = deleted_links[d.source] + 1 || 1; deleted_links[d.target] = deleted_links[d.target] + 1 || 1; @@ -38,125 +60,101 @@ return res; }); - var edges = [], - nodes = []; - for (var i = 0; i < selectedNodes.length; i++) { - nodes.push(i); - } - for (var i = 0; i < selectedLinks.length; i++) { - var edge = selectedLinks[i]; - edge.weight = edge.value; - edges.push(edge); - } - - // Create the "community" - var community = jLouvain().nodes(nodes).edges(edges), - res = community(); - // Affect community for each node - for (var key in res) { - selectedNodes[nodes[key]].group = res[key]; - } - - var link = svg.append("g") - .attr("class", "links") - .selectAll("line") - .data(selectedLinks) - .enter().append("line") - .attr("stroke-width", function(d) { - return d.value; - }); - - link.append("title") - .text(function(d) { - return d.value; - }); - - var node = svg.append("g") - .attr("class", "nodes") - .selectAll("circle") - .data(selectedNodes) - .enter().append("circle") - .attr("r", function(d) { - return 7; - // return 10 * d.specificity; - }) - .attr("fill", function(d) { - return color(d.group); - }) - .call(d3.drag() - .on("start", dragstarted) - .on("drag", dragged) - .on("end", dragended)) - .on("click", function(d) { - window.open('/' + corpus.in + '/' + d.data.id + '.txt'); - }) - .on("mouseover", function(d) { - d3.select("#subtitle") - .style("opacity", 1) - .text(() => { - return d3.select(this).selectAll("title").text(); - }); - d3.select(this) - .attr("r", 14); - }) - .on("mouseleave", function(d) { - d3.select("#subtitle") - .style("opacity", 0); - d3.select(this) - .attr("r", 7); - }); - - node.append("title") - .text(function(d) { - return d.data.id; - }); - + // FILL CANVAS simulation .nodes(selectedNodes) - .on("tick", ticked); + .on("tick", draw); simulation.force("link") .links(selectedLinks); - function ticked() { - 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; - }); + function draw() { + context.save(); + context.clearRect(0, 0, width, height); + context.translate(transform.x, transform.y); + context.scale(transform.k, transform.k); - node - .attr("cx", function(d) { - return d.x; - }) - .attr("cy", function(d) { - return d.y; - }); + // draw links + context.strokeStyle = "#546e7a"; + context.beginPath(); + graph.links.forEach(function(d) { + context.moveTo(d.source.x, d.source.y); + context.lineTo(d.target.x, d.target.y); + }); + context.stroke(); + + // draw nodes + graph.nodes.forEach(function(d) { + context.beginPath(); //for each node do begin path as context fill style and stroke are different. + context.strokeStyle = "#fff"; + context.fillStyle = color(d.group); + context.moveTo(d.x + radius, d.y); + context.arc(d.x, d.y, radius, 0, 2 * Math.PI); + context.fill(); + context.stroke(); + }); + context.restore(); } + + // ZOOM + var transform = d3.zoomIdentity; + + d3Canvas + .call(d3.drag().subject(dragsubject) + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)) + .call(d3.zoom().scaleExtent([-100, 100]).on("zoom", zoomed)) + .call(draw); + + function zoomed() { + transform = d3.event.transform; + draw(); + } + + function dragsubject() { + var selectedNode, + i, + n, + x = transform.invertX(d3.event.x), + y = transform.invertY(d3.event.y), + dx, + dy; + + for (i = selectedNodes.length - 1; i >= 0; --i) { + selectedNode = selectedNodes[i]; + dx = x - selectedNode.x; + dy = y - selectedNode.y; + if (dx * dx + dy * dy < radius * radius) { + selectedNode.x = transform.applyX(selectedNode.x); + selectedNode.y = transform.applyY(selectedNode.y); + return selectedNode; + } + } + } + + // function dragged() { + // d3.event.subject.x = transform.invertX(d3.event.x); + // d3.event.subject.y = transform.invertY(d3.event.y); + // draw(); + // } + + function dragstarted() { + if (!d3.event.active) simulation.alphaTarget(0.3).restart(); + d3.event.subject.fx = transform.invertX(d3.event.subject.x); + d3.event.subject.fy = transform.invertY(d3.event.subject.y); + } + + function dragged() { + d3.event.subject.fx = transform.invertX(d3.event.x); + d3.event.subject.fy = transform.invertY(d3.event.y); + } + + function dragended() { + if (!d3.event.active) simulation.alphaTarget(0); + d3.event.subject.fx = null; + d3.event.subject.fy = null; + } + }); - - function dragstarted(d) { - if (!d3.event.active) simulation.alphaTarget(0.3).restart(); - d.fx = d.x; - d.fy = d.y; - } - - function dragged(d) { - d.fx = d3.event.x; - d.fy = d3.event.y; - } - - function dragended(d) { - if (!d3.event.active) simulation.alphaTarget(0); - d.fx = null; - d.fy = null; - } } \ No newline at end of file diff --git a/public/js/rendering-optimization.js b/public/js/rendering-optimization.js new file mode 100644 index 0000000..e9c314c --- /dev/null +++ b/public/js/rendering-optimization.js @@ -0,0 +1,26 @@ +(function() { + var lastTime = 0; + var vendors = ['webkit', 'moz']; + for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; + window.cancelAnimationFrame = + window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { + callback(currTime + timeToCall); + }, + timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; +}()); \ No newline at end of file diff --git a/views/d3.pug b/views/d3.pug index 51e13af..a3c56d9 100644 --- a/views/d3.pug +++ b/views/d3.pug @@ -9,8 +9,9 @@ pre code#subtitle div - svg#container + canvas#container script(type="application/javascript" src="https://d3js.org/d3.v4.min.js") + script(type="application/javascript" src=publicDir + "/js/rendering-optimization.js") script(type="application/javascript" src=publicDir + "/js/jLouvain.js") script(type="application/javascript" src=publicDir + "/js/resize.js") script(type="application/javascript" src=publicDir + "/js/force-directed-graph.js")